Je souhaite créer une alternative à std::type_index qui ne nécessite pas de RTTI:

template <typename T>
int* type_id() {
    static int x;
    return &x;
}

Notez que l'adresse de la variable locale x est utilisée comme ID de type, pas la valeur de x elle-même. De plus, je n'ai pas l'intention d'utiliser un simple pointeur dans la réalité. Je viens de supprimer tout ce qui n'est pas pertinent pour ma question. Consultez ma type_index mise en œuvre actuelle.

Cette approche est-elle valable, et si oui, pourquoi? Sinon, pourquoi pas? J'ai l'impression d'être sur un terrain instable ici, alors je m'intéresse aux raisons précises pour lesquelles mon approche fonctionnera ou ne fonctionnera pas.

Un cas d'utilisation typique pourrait être d'enregistrer des routines au moment de l'exécution pour gérer des objets de différents types via une seule interface:

class processor {
public:
    template <typename T, typename Handler>
    void register_handler(Handler handler) {
        handlers[type_id<T>()] = [handler](void const* v) {
            handler(*static_cast<T const*>(v));
        };
    }

    template <typename T>
    void process(T const& t) {
        auto it = handlers.find(type_id<T>());
        if (it != handlers.end()) {
            it->second(&t);
        } else {
            throw std::runtime_error("handler not registered");
        }
    }

private:
    std::map<int*, std::function<void (void const*)>> handlers;
};

Cette classe peut être utilisée comme ceci:

processor p;

p.register_handler<int>([](int const& i) {
    std::cout << "int: " << i << "\n";
});
p.register_handler<float>([](float const& f) {
    std::cout << "float: " << f << "\n";
});

try {
    p.process(42);
    p.process(3.14f);
    p.process(true);
} catch (std::runtime_error& ex) {
    std::cout << "error: " << ex.what() << "\n";
}

Conclusion

Merci à tous pour votre aide. J'ai accepté la réponse de @StoryTeller car il a expliqué pourquoi la solution devrait être valide selon les règles de C ++. Cependant, @SergeBallesta et un certain nombre d'autres dans les commentaires ont souligné que MSVC effectue des optimisations qui sont malheureusement sur le point de casser cette approche. Si une approche plus robuste est nécessaire, alors une solution utilisant std::atomic peut être préférable, comme suggéré par @galinette:

std::atomic_size_t type_id_counter = 0;

template <typename T>
std::size_t type_id() {
    static std::size_t const x = type_id_counter++;
    return x;
}

Si quelqu'un a d'autres pensées ou informations, j'ai toujours hâte de les entendre!

40
Joseph Thomson 26 janv. 2017 à 09:41

4 réponses

Meilleure réponse

Oui, ce sera correct dans une certaine mesure. Les fonctions de modèle sont implicitement inline, et les objets statiques dans les fonctions inline sont partagés entre toutes les unités de traduction.

Ainsi, dans chaque unité de traduction, vous obtiendrez l'adresse de la même variable locale statique pour l'appel à type_id<Type>(). Vous êtes ici protégé contre les violations ODR par la norme.

Par conséquent, l'adresse de la statique locale peut être utilisée comme une sorte d'identificateur de type à l'exécution maison.

27
Cody Gray 26 janv. 2017 à 10:28

Modification post-commentaire : je ne me suis pas rendu compte au début de la lecture que l'adresse était utilisée comme clé, pas comme valeur int. C'est une approche intelligente, mais elle souffre à mon humble avis d'un défaut majeur: l ' intention n'est pas claire si quelqu'un d'autre trouve ce code.

Cela ressemble à un vieux hack C. C'est intelligent, efficace, mais le code ne s'explique pas du tout en quoi consiste l'intention. Ce qui en C ++ moderne, à mon humble avis, est mauvais. Écrivez du code pour les programmeurs, pas pour les compilateurs. Sauf si vous avez prouvé qu'il existe un sérieux goulot d'étranglement qui nécessite une optimisation bare metal.

Je dirais que cela devrait fonctionner mais je ne suis clairement pas un juriste en langues ...

Une solution constexpr élégante, mais complexe, peut être trouvée ici ou ici

Réponse originale

Il est "sûr" dans le sens où c'est du c ++ valide et vous pouvez accéder au pointeur renvoyé dans tout votre programme, car le local statique sera initialisé au premier appel de fonction. Il y aura une variable statique par type T utilisée dans votre code.

Mais :

  • Pourquoi renvoyer un pointeur non const? Cela permettra aux appelants de modifier la valeur de la variable statique, ce qui n'est clairement pas quelque chose que vous aimeriez
  • Si vous retournez un pointeur const, je ne vois aucun intérêt à ne pas retourner par valeur au lieu de renvoyer le pointeur

De plus, cette approche pour obtenir un identifiant de type ne fonctionnera qu'au moment de la compilation, pas au moment de l'exécution avec des objets polymorphes. Ainsi, il ne retournera jamais le type de classe dérivé à partir d'une référence de base ou d'un pointeur.

Comment allez-vous initialiser les valeurs int statiques? Ici, vous ne les initialisez pas donc ce n'est pas valide. Peut-être que vous vouliez utiliser le pointeur non const pour les initialiser quelque part?

Il y a deux meilleures possibilités:

1) Spécialisez le modèle pour tous les types que vous souhaitez prendre en charge

template <typename T>
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

template <>
int type_id<char>() {
    static const int id = 0;
    return id;  //or : return 0
}

template <>
int type_id<unsigned int>() {
    static const int id = 1;
    return id;  //or : return 1
}

//etc...

2) Utiliser un compteur global

std::atomic<int> typeInitCounter = 0;

template <typename T>
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

Cette dernière approche est meilleure à mon humble avis car vous n'avez pas à gérer les types. Et comme l'a souligné A.S.H, le compteur incrémenté à base zéro permet d'utiliser un vector au lieu d'un map, ce qui est beaucoup plus simple et efficace.

Aussi, utilisez un unordered_map au lieu d'un map pour cela, vous n'avez pas besoin de commande. Cela vous donne un accès O (1) au lieu de O (log (n))

6
Community 23 mai 2017 à 11:33

Comme mentionné par @StoryTeller, cela fonctionne très bien au moment de l'exécution .
Cela signifie que vous ne pouvez pas l'utiliser comme suit:

template<int *>
struct S {};

//...

S<type_id<char>()> s;

De plus, ce n'est pas un identifiant fixe. Par conséquent, vous n'avez aucune garantie que char sera lié à la même valeur à travers différentes exécutions de votre exécutable.

Si vous pouvez gérer ces limitations, c'est très bien.


Si vous connaissez déjà les types pour lesquels vous voulez un identifiant persistant, vous pouvez utiliser quelque chose comme ça à la place (en C ++ 14):

template<typename T>
struct wrapper {
    using type = T;
    constexpr wrapper(std::size_t N): N{N} {}
    const std::size_t N;
};

template<typename... T>
struct identifier: wrapper<T>... {
    template<std::size_t... I>
    constexpr identifier(std::index_sequence<I...>): wrapper<T>{I}... {}

    template<typename U>
    constexpr std::size_t get() const { return wrapper<U>::N; }
};

template<typename... T>
constexpr identifier<T...> ID = identifier<T...>{std::make_index_sequence<sizeof...(T)>{}};

Et crée vos identifiants comme suit:

constexpr auto id = ID<int, char>;

Vous pouvez utiliser ces identifiants plus ou moins comme vous l'avez fait avec votre autre solution:

handlers[id.get<T>()] = ...

De plus, vous pouvez les utiliser partout où une expression constante est requise.
À titre d'exemple en tant que paramètre de modèle:

template<std::size_t>
struct S {};

// ...

S<id.get<B>()> s{};

Dans une instruction switch:

    switch(value) {
    case id.get<char>():
         // ....
         break;
    case id.get<int>():
        // ...
        break;
    }
}

Etc. Notez également qu'ils sont persistants à travers des exécutions différentes tant que vous ne changez pas la position d'un type dans la liste de paramètres de modèle de ID.

Le principal inconvénient est que vous devez connaître tous les types pour lesquels vous avez besoin d'un identifiant lorsque vous introduisez la variable id.

6
Community 23 mai 2017 à 11:33

Ceci est cohérent avec le standard car C ++ utilise des modèles et non des génériques avec l'effacement de type comme Java, donc chaque type déclaré aura sa propre implémentation de fonction contenant une variable statique. Toutes ces variables sont différentes et doivent donc avoir des adresses différentes.

Le problème est que leur valeur n'est jamais utilisée et pire, jamais modifiée. Je me souviens que les optimiseurs peuvent fusionner des constantes de chaîne. Comme les optimiseurs font de leur mieux pour être beaucoup plus intelligents que n'importe quel programmeur humain, j'aurai peur qu'un compilateur d'optimisation trop zélé découvre que comme ces valeurs variables ne sont jamais modifiées, elles garderont toutes une valeur 0, alors pourquoi ne pas les fusionner toutes pour économiser de la mémoire?

Je sais qu'en raison de la règle comme si, le compilateur est libre de faire ce qu'il veut à condition que les résultats observables soient les mêmes. Et je ne suis pas sûr que les adresses des variables statiques qui partageront toujours la même valeur soient différentes ou non. Peut-être que quelqu'un pourrait confirmer quelle partie de la norme en tient vraiment compte?

Les compilateurs actuels compilent toujours séparément les unités de programme, de sorte qu'ils ne peuvent pas être sûrs qu'une autre unité de programme utilisera ou modifiera la valeur. Donc, à mon avis, l'optimiseur n'aura pas assez d'informations pour décider de fusionner la variable et votre modèle est sûr.

Mais comme je ne pense vraiment pas que ce standard le protège, je ne peux pas dire si les futures versions des constructeurs C ++ (compilateur + éditeur de liens) n'inventeront pas une phase d'optimisation globale recherchant activement des variables inchangées qui pourraient être fusionnées. Plus ou moins la même chose qu'ils recherchent activement UB pour optimiser des parties de code ... Seuls les modèles courants, où ne pas les autoriser casserait une base de code trop grande, en sont protégés, et je ne pense pas que le vôtre soit assez courant.

Un moyen plutôt piraté d'éviter une phase d'optimisation pour fusionner des variables ayant la même valeur serait simplement de donner à chacune une valeur différente:

int unique_val() {
    static int cur = 0;  // normally useless but more readable
    return cur++;
}
template <typename T>
void * type_id() {
    static int x = unique_val();
    return &x;
}

Ok, cela n'essaye même pas d'être thread-safe, mais ce n'est pas un problème ici: les valeurs ne seront jamais utilisées par elles-mêmes. Mais vous avez maintenant différentes variables ayant une durée statique (selon 14.8.2 de la norme comme dit @StoryTeller), qui sauf dans des conditions de course ont des valeurs différentes. Comme ils sont utilisés ou utilisés, ils doivent avoir des adresses différentes et vous devriez être protégé pour une amélioration future de l'optimisation des compilateurs ...

Remarque: je pense que comme la valeur ne sera pas utilisée, renvoyer un void * sonne plus propre ...


Juste un ajout volé à un commentaire de @bogdan. MSVC est connu pour avoir une optimisation très agressive avec l'indicateur /OPT:ICF. La discussion suggère que cela ne devrait pas être conforme, et que cela ne s'applique qu'aux variables marquées comme const. Mais cela renforce mon opinion que même si le code d'OP semble conforme, je n'oserais pas l'utiliser sans précautions supplémentaires dans le code de production.

12
Serge Ballesta 26 janv. 2017 à 15:54