40 votes

Est-il sécuritaire d'utiliser l'adresse d'une variable locale statique à l'intérieur d'un modèle de fonction comme un identificateur de type de?

Je souhaite créer une alternative à l' 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 type ID, et non de la valeur de x lui-même. Aussi, je n'ai pas l'intention d'utiliser un pointeur nu dans la réalité. J'ai juste enlevé tout n'est pas pertinente à ma question. Voir mon effectif type_index mise en œuvre ici.

Est-ce l'approche de son, et si oui, pourquoi? Si non, pourquoi pas? J'ai l'impression que je suis sur des bases fragiles, ici, donc je suis intéressé par les raisons précises pour lesquelles mon approche sera ou ne sera pas de travail.

Un cas d'utilisation typique pourrait être d'enregistrer des routines au moment de l'exécution de manipuler des objets de types différents à travers une interface unique:

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 suit:

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 @Conteur, comme il l'a expliqué pourquoi la solution doit être valide selon les règles de C++. Toutefois, @SergeBallesta et un certain nombre d'autres dans les commentaires ont souligné que MSVC effectue des optimisations qui viennent trop près de la rupture de cette approche. Si une approche plus rigoureuse est nécessaire, puis une solution à l'aide d' 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 des pensées ou des renseignements, je suis toujours avide de l'entendre!

27voto

StoryTeller Points 6139

Oui, ça va être juste à point. Modèle de fonctions sont implicitement inline, et les objets statiques en inline fonctions sont partagées 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 à la type_id<Type>(). Vous êtes protégé, ici, à partir de l'ODR de violations par la norme.

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

11voto

Serge Ballesta Points 12850

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

Le problème, c'est que leur valeur n'est jamais utilisé et pour le pire, n'a jamais changé. Je me souviens que les optimiseurs pouvez fusionner les constantes de chaîne. Comme les optimiseurs de faire de leur mieux pour être beaucoup plus intelligent que n'importe quel programmeur humain, j'aurai peur qu'une trop zélé compilateur optimisant découvrir que ces valeurs de la variable sont jamais changé, ils seront tous de garder une valeur 0, alors pourquoi ne pas les fusionner ensemble pour économiser de la mémoire?

Je le sais parce que de la que si la règle, le compilateur est libre de faire ce qu'il veut à condition les résultats observables sont les mêmes. Et je ne suis pas sûr que les adresses des variables statiques qui aura toujours la même valeur doit être différente ou pas. Peut-être que quelqu'un pourrait confirmer ce que la partie de la norme se soucie pour elle?

Compilateurs gcc toujours compiler séparément les unités de programme, de sorte qu'ils ne peuvent pas être sûr de savoir si une autre unité de programme permettra d'utiliser ou de modifier la valeur. Donc, mon opinion est que l'optimiseur de ne pas avoir suffisamment d'informations pour décider de fusionner la variable, et le patron est sûr.

Mais comme je ne pense vraiment pas que la norme protège, je ne peux pas dire si les futures versions de C++ constructeurs (compilateur + liens) ne va pas inventer un mondial de l'optimisation de la phase de recherche active inchangée variables qui pourraient être fusionnés. Plus ou moins la même chose qu'ils participent activement à la recherche d'UB pour optimiser certaines parties de code... Seulement des modèles communs, où leur permet pas de casser une trop grande base de code sont protégés, et je ne pense pas que le vôtre est assez commun.

Plutôt hacky façon de prévenir une optimisation de la phase de fusion des variables ayant la même valeur serait juste pour donner à chacun 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, ce n'essaye même pas de thread-safe, mais il n'est pas un problème ici: les valeurs ne seront jamais utilisées par eux-mêmes. Mais vous avez maintenant les différentes variables ayant la durée statique (par 14.8.2 de la norme comme dit par @Conteur), que , sauf dans des conditions de course ont des valeurs différentes. Comme ils sont odr utilisés, ils doivent avoir des adresses différentes et vous devez vous protéger pour l'avenir de l'amélioration de l'optimisation des compilateurs...

Remarque: je pense que la valeur ne sera pas utilisée, le retour d'un void * sons plus propre...


Juste un ajout de vol à partir d'un commentaire de @bogdan. MSVC est connu pour avoir de très agressives d'optimisation avec l' /OPT:ICF drapeau. La discussion suggèrent que est ne devrait pas être conforme, et qu'il s'applique uniquement à la variable marqué comme const. Mais il renforce mon opinion que, même si l'OP du code semble conforme, je n'oserai pas l'utiliser sans précautions supplémentaires dans le code de production.

6voto

galinette Points 3086

Post-commentaire edit : je ne savais pas à la première lecture que l'adresse a été utilisée en tant que clé, pas la valeur int. C'est une approche astucieuse, mais il souffre à mon humble avis un défaut majeur : l' intention est très difficile de savoir si quelqu'un d'autre trouve que le code.

Il ressemble à un vieux C hack. Il est intelligent, efficace, mais le code n'est pas auto-expliquer à tous ce que l'intention est. Qui en c++ moderne, à mon humble avis, est mauvais. Écrire du code pour les programmeurs, pas pour les compilateurs. Sauf si vous avez fait la preuve qu'il existe un sérieux goulot d'étranglement qui nécessite métal nu optimisation.

Je dirais que cela devrait fonctionner, mais je suis clairement pas une langue de l'avocat...

Un élégant, mais complexe constexpr solution, peut être trouvé ici ou ici

Réponse originale à cette question

C'est "safe" dans le sens que c'est valide en c++ et vous pouvez accéder au pointeur retourné dans tout votre programme, comme la statique locale sera initialisé au premier appel de la fonction. Il y aura une variable statique par type T utilisé dans votre code.

Mais :

  • Pourquoi le retour d'un non const pointeur? Cela va permettre aux utilisateurs de modifier la variable statique de valeur, qui n'est clairement pas quelque chose que vous aimeriez
  • En cas de retour d'un pointeur const, je ne vois aucun intérêt à ne pas revenir en valeur au lieu de retourner le pointeur

Aussi, cette approche pour l'obtention d'un id de type fonctionnera uniquement au moment de la compilation, et non pas au moment de l'exécution avec des objets polymorphes. Donc, il ne sera jamais de retour de la classe dérivée type à partir d'une base de référence ou un pointeur.

Comment allez-vous initialiser le static int valeurs? Ici, vous n'avez pas les initialiser ce n'est donc pas valide. Peut-être que vous voulez utiliser la non const pointeur pour l'initialisation de quelque part?

Il y a deux meilleures possibilités:

1)se Spécialisent le modèle pour tous les types que vous souhaitez soutenir

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 à mon humble avis mieux parce que vous n'avez pas à gérer les types. Et comme l'a souligné A. S. H, à zéro compteur incrémenté permet d'utiliser l' vector au lieu de map ce qui est beaucoup plus simple et efficace.

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

6voto

skypjack Points 5516

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

template<int *>
struct S {};

//...

S<type_id<char>()> s;

En outre, il n'est pas un identificateur fixe. Par conséquent, vous n'avez aucune garantie qu' char sera lié à la même valeur par le biais des différents runnings de votre exécutable.

Si vous pouvez faire face à ces limitations, il est juste bien.


Si vous connaissez déjà les types pour lesquels vous souhaitez un identificateur persistant, vous pouvez utiliser quelque chose comme ceci à 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 il 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>()] = ...

En outre, vous pouvez les utiliser partout où une expression constante est nécessaire.
Comme un exemple comme un paramètre du 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;
    }
}

Et ainsi de suite. Notez également qu'ils sont persistants à travers différents runnings tant que vous ne modifiez pas la position d'un type dans le modèle de la liste des paramètres de l' 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 de l' id variable.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X