Comportement interessant a partir de friend injection

Le problème exposé dans ce sujet a été résolu.

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]).

source

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 ?

+0 -0

Les template sont régis par une règle qui consiste à n’instancier qu’une pour les mêmes paramètres. Ce qui veut dire que pour ID<c>, la valeur est calculée à la première instanciation puis réutilisée pour les autres. Ce qui est normal puisque c’est une valeur constexpr.

Ce comportement est identique pour tous les types, y comprit les fonctions. Sauf qu’une fonction, même constexpr, peut retourner des valeurs différentes, son code est donc re-exécuté à chaque fois ce qui permet d’avoir un Next() qui s’incrémente.

Il y a quand même une subtilité qui fait que 2 Counters commence à 0: une valeur par défaut comme lambda donne 2 types différents, c’est-à-dire que Counter{} != Counter{}.

Du coup, si tu veux un ID qui s’incrémente à chaque fois, il faut une instanciation différente pour chaque appel en ajoutant une lambda comme paramètre par défaut.

template <std::same_as<Counter> C, auto = []{}>
inline constexpr auto ID = C.Next();

static_assert(ID<c> == 1);
static_assert(ID<c> == 2); // ok
static_assert(ID<c> == 3); // ok

Ah oui en fait c’est assez bete de ma part parce que c’est le meme raisonnement que UpdateCurrentState, dans le sens ou j’ai besoin de l’instancier une nouvelle fois pour qu’elle evalue une nouvelle fois le if constexpr et decide de la branche a prendre. Ainsi chaque appel a UpdateCurrentState generera une nouvelle instantiation grace a la lambda en parametre template et permettera de reevaluer le if constexpr. Du coup mon probleme avec ID est similaire si j’ai bien compris puisque je dois l’instancier a chaque appel pour que C.Next() soit egalement de nouveau evaluee ?

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte