Reflection statique

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

Pourrait-on m’expliquer comment ce code fonctionne ? Plus particulierment le fonctionnement de arg_t et de num_constructor_args.

template <typename...> struct list_t {};
namespace detail {
    template <typename T, std::size_t N> struct member {
        friend constexpr auto member_type(member<T, N>);
    };

    template <typename T, std::size_t N, typename U>
    struct set_member_type : std::true_type {
        friend constexpr auto member_type(member<T, N>) { return U{}; }
    };

    template <typename T, std::size_t N> struct arg_t {
        template <typename U>
        constexpr operator U() const noexcept(std::enable_if_t<not std::is_same_v<T, U>, set_member_type<T, N, U>>::value);
    };

    template <typename T, typename... TArgs>
    constexpr auto is_brace_constructible(int) -> decltype(T{TArgs{}...}, std::true_type{});

    template <typename T, typename... TArgs>
    constexpr auto is_brace_constructible(...) -> std::false_type;

    template <typename T, auto... Is>
    constexpr auto is_constructible(std::index_sequence<Is...>) -> decltype(is_brace_constructible<T, arg_t<T, Is>...>(0));

    template <typename T, std::size_t L = 0u, std::size_t H = sizeof(T) + 1u>
    constexpr auto num_constructor_args() -> std::size_t {
    if constexpr (H - L == 1u) {
        return L;
    } else {
        constexpr auto M = (L + H + 1u) / 2u;
        if constexpr (decltype(is_constructible<T>(std::make_index_sequence<M>{})){}) {
                return num_constructor_args<T, M, H>();
            } else {
                return num_constructor_args<T, L, M>();
            }
        }
    }

    template <typename T, auto... Is>
    constexpr auto constructor_args(std::index_sequence<Is...>) -> list_t<decltype(member_type(member<T, Is>{}))...>;

    template <typename T> 
    constexpr auto constructor_args() -> list_t<>;
} // namespace detail

template <typename T>
using num_members_t = std::integral_constant<std::size_t, detail::num_constructor_args<T>()>;

template <typename T>
using members_list_t = decltype(detail::constructor_args<T>(std::make_index_sequence<num_members_t<T>::value>{}));

struct Foo {
    int a;
    bool b;
    char c;
    double d;
    std::string e;
};
static_assert(std::is_same_v<members_list_t<Foo>, list_t<int, bool, char, double, std::string>>);

Le but de arg_t est de convertir n’importe quoi en un type désiré et d’y associer un index (la position de l’argument) pour pouvoir ensuite l’utiliser dans constructor_args. Avec l’histoire de friend qui permet d’associer une fonction à un type, comme pour Counter.

num_constructor_args est une construction en essayant un nombre d’argument de manière dichotomique (pour je suppose accélérer la vitesse de compilation).

Je ne sais pas ce que tu regardes, mais cela ressemble à ce qui se fait pour magicget ou boost.pfr. De mémoire, il y avait un article qui expliquaient en long et en travers avec différente technique, mais je ne le retrouve pas (l’implémentation que tu montres a quelques pièges quand il y a de l’héritage).

Je comprends que l’instantiation de arg_t pour un type T et un index Is dans

template <typename T, auto... Is>
constexpr auto is_constructible(std::index_sequence<Is...>) -> decltype(is_brace_constructible<T, arg_t<T, Is>...>(0));

implique une instantiation de set_member_type toujours pour un type T et un index Is. Je pense que l’instantiation de set_member_type est requise a travers l’operateur noexcept dans la definition de arg_t (corrige moi si ce n’est pas le cas) et cette instantiation injectera par consequent la definition de member retournant U{} ou U serait le type d’un membre de T a l’index Is et ou arg_t serait implicitement convertible vers U. Ainsi, dans

template <typename T, auto... Is>
constexpr auto is_constructible(std::index_sequence<Is...>) -> decltype(is_brace_constructible<T, arg_t<T, Is>...>(0));

Le type de retour de constructor_args est list_t<decltype(member_type(member<T, Is>{}))...> ou member_type(member<T, Is>) sera valide puisque la definition de member_type est desormais accessible et retournera le membre de T selon un index Is, soit U{}.


Ce que je ne comprends pas c’est selon la definition de arg_t.

template <typename T, std::size_t N> struct arg_t {
    template <typename U>
    constexpr operator U() const noexcept(std::enable_if_t<not std::is_same_v<T, U>, set_member_type<T, N, U>>::value);
};

Comment est-ce que U est deduit ici dans la mesure ou arg_t est implicitement convertible vers U ?

template <typename T, auto... Is>
constexpr auto is_constructible(std::index_sequence<Is...>) -> decltype(is_brace_constructible<T, arg_t<T, Is>...>(0));

EDIT: erreur de ma part arg_t est instancie dans is_brace_constructible et non dans is_constructible

+0 -0

Je crois que arg_t est converti vers TArgs

template <typename T, typename... TArgs>
constexpr auto is_brace_constructible(int) -> decltype(T{TArgs{}...}, std::true_type{});

lorsque

template <typename T, auto... Is>
constexpr auto is_constructible(std::index_sequence<Is...>) -> decltype(is_brace_constructible<T, arg_t<T, Is>...>(0));

est appele. Du coup j’imagine que c’est comme ca que l’on recupere le type d’un membre ?

Du coup voici ma theorie. Pour ce code

template <typename T, std::size_t N>
struct member {
    friend constexpr auto member_type(member<T, N>);
};

template <typename T, std::size_t N, typename U>
struct set_member_type : std::true_type {
    friend constexpr auto member_type(member<T, N>) { return U{}; }
};

template <typename T, std::size_t N>
struct arg_t {
    template <typename U>
    constexpr operator U() const noexcept(std::enable_if_t<!std::is_same_v<T, U>, set_member_type<T, N, U>>::value);
};

template <typename T, typename ...TArgs>
constexpr auto is_brace_constructible(int) -> decltype(T{TArgs{}...}, std::true_type{});

template <typename T, typename ...TArgs>
constexpr auto is_brace_constructible(...) -> std::false_type;

template <typename T, auto ...Is>
constexpr auto is_constructible(std::index_sequence<Is...>) -> decltype(is_brace_constructible<T, arg_t<T, Is>...>(0));

struct Foo {
    int i;
    float f;
};

static_assert(requires { is_constructible<Foo>(std::index_sequence<1, 2>{}); });

static_assert(std::is_same_v<decltype(member_type(member<Foo, 1>{})), int>);
static_assert(std::is_same_v<decltype(member_type(member<Foo, 2>{})), float>);

On a une fonction friend member_type qui est declaree dans member et defini dans set_member_type ou elle retourne U{}. T correspond au type de notre structure et N l’index sur les membres de la structure (si l’on veut acceder au premier, 2e, 3e, etc…). Ensuite c’est la ou j’ai eu beaucoup de mal a comprendre : arg_t. C’est une structure qui implicitement convertible en n’importe quel type U, grace a l’operateur de conversion templated. L’operateur noexcept permet quant a lui d’instancier set_member_type si T et U ne sont pas les memes types. On a ensuite une fonction is_brace_constructible qui prend en argument template le type de notre structure (soit T) et les types de ses arguments (soit TArgs). Cette fonction a pour type de retour std::true_type si T{TArgs{}…} est valide, sinon elle a pour type de retour std::false_type. La fonction is_constructible prend en parametre template le type de notre structure (toujours T) et une sequence d’index Is et va appeler is_brace_constructible avec arg_t de telle maniere que chaque arg_t<T, Is> sera convertie en un TArgs selon un index. Ainsi, le parametre template U de l’operateur de conversion de arg_t sera egal a un type d’un membre de T et puisque l’on instancie arg_t, set_member_type l’est egalement et permet de conserver le type U. On repete l’operation pour tous les indexs de notre index_sequence. A la fin, pour une structure Foo du type :

struct Foo {
    int i;
    float f;
};

on va appeler is_constructible en verifier que l’expression est valide

static_assert(requires { is_constructible<Foo>(std::index_sequence<1, 2>{}); });

is_constructible va appeler arg_t pour chaque membre de Foo et l’instanciation de arg_t permet de stocker le type de chaque membre dans member_type qui sera alors desormais accessible et defini grace a l’instantiation de set_member_type. Ainsi,

decltype(member_type(member<Foo, 1>{}); // int
decltype(member_type(member<Foo, 2>{}); // float

Est-ce que c’est correct ?

+0 -0

Tu as compris le truc.

Pour l’héritage, je ne sais plus ce qui était fait. Je suppose qu’il faut prendre le premier argument et utiliser std::is_base_of pour vérifier. Si vrai, le faire pour le second et ainsi de suite.

Aussi, une recherche dichotomique trop naïve cause quelques problèmes, il faut ajouter des vérifications sur chaque membre.

struct A
{
  int a;
  int b;
};

struct B : A
{
  int c;
  A   d
  int e[3];
};

int main()
{
  B{ 1,  2,   3,       4,  5,         6,  7};
  //{A.a A.b} B.c B.d={A.a A.b} B.e={[0] [1]}
}
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