Bonjour, j’ai recemment trouve quelque chose d’assez curieux avec les injections friend. Prenons ce code :
template <auto InstanceIdentifier, auto x>
struct Flag {
friend consteval auto adl_func(Flag, auto...);
};
template <auto InstanceIdentifier, auto x>
struct InjectDefinition {
friend consteval auto adl_func(Flag<InstanceIdentifier, x>, auto...) {}
};
template<auto InstanceIdentifier, auto x = 0, auto = []{}>
constexpr auto UpdateCurrentState() {
if constexpr (requires { adl_func(Flag<InstanceIdentifier, x>{}); }) {
return UpdateCurrentState<InstanceIdentifier, x + 1>();
}
else {
InjectDefinition<InstanceIdentifier, x>{};
}
return x;
}
template <auto InstanceIdentifier = []{}>
struct Counter {
template <auto x = UpdateCurrentState<InstanceIdentifier>()>
consteval auto Next() const { return x; }
};
Nous avons une fonction friend qui est declare a travers la template struct Flag qui prend deux parametres NTTP (je reviens la dessus plus tard) et qui est defini dans la template struct InjectDefinition qui prend aussi deux parametres NTTP et qui comme son nom l’indique sert a rendre la definition de adl_func accessible. A noter que selon la specification :
As with non-template classes, the names of namespace-scope friend functions of a class template specialization are not visible during an ordinary lookup unless explicitly declared at namespace scope ([class.friend]). Such names may be found under the rules for associated classes ([basic.lookup.argdep]).
Il n’est donc possible d’acceder a la fonction qu’a travers ADL ou en creant une declaration de celle-ci dans le scope dans lequel on veut l’acceder.
Flag et InjectDefinition ont tous deux, deux parametres tempalte : InjectDefinition et x. x correspond a un index (J’expliquerai plus tard le role de InjectDefinition). Ainsi, une instantiation de InjectDefinition avec les memes valeurs en template (i.e, le meme index x et le meme InstanceIdentifer) que Flag permettera de rendre adl_func pour une certaine valeur x accessible a tout le global scope, a travers ADL.
Vient ensuite la fonction UpdateCurrentState. Elle a pour but de verifier si adl_func est defini pour x = 0 (c’est a dire si InjectDefinition pour x = 0 a ete instantie) et si ce n’est pas le cas elle instanciera InjectDefinition pour x = 0, rendant donc la definition de adl_func accessible toujours pour x = 0 et on retourne x a la fin. Si c’est le cas, alors on reitere l’operation pour x = 1.
Vient ensuite la structure Counter qui possede une fonction template membre Use qui retourne la valeur retournee par UpdateCurrrentState.
Enfin, InstanceIdentifier est une lambda, sauf que puisque chaque closure type associe a une lambda est unique, chaque appel a Counter entrainera une nouvelle instanciation de template et puisque que l’on passe cet InstanceIdentifier a Flag, InjectDefinition et UpdateCurrentState, ils seront tous dependants de l’instantion de template de Counter (si on cree une nouvelle variable Counter, on aura une autre instantiation avec un Flag, InjectDefinition, et UpdateCurrentState associes).
Par exemple
template <auto InstanceIdentifier>
consteval auto bar() { ... }
template <auto InstanceIdentifier, auto x = bar<InstanceIdentifier>()>
struct Foo {
static constexpr auto value = x;
};
auto f1 = Foo<[]{}>::value; // premiere instantiation de Foo et de bar
auto f2 = Foo<[]{}>::value; // nouvelle instantiation de Foo et de bar
// => f2 a un type different de f1 et n'a pas ete init avec la meme fonction
static_assert(std::is_same_v<decltype(f1), decltype(f2)>);
Du coup pour revenir a Counter, UpdateCurrentState permet de faire un compteur compile time.
constexpr auto c = Counter{};
static_assert(c.Next() == 1);
static_assert(c.Next() == 2);
static_assert(c.Next() == 3);
Ce que je ne comprends pas c’est pourquoi cela ne fonctionne pas a travers un template
constexpr auto c = Counter{};
template <Counter C>
inline constexpr auto ID = C.Next();
static_assert(ID<c> == 1);
static_assert(ID<c> == 2); // fails
static_assert(ID<c> == 3); // fails
Pourquoi ID<c>
est toujours egal a 1 ?