web-dev-qa-db-fra.com

utilisation de SFINAE pour la spécialisation de classe de modèle

suppose que j'ai ces déclarations

template<typename T> class User;
template<typename T> class Data;

et que vous souhaitez mettre en œuvre User<> pour T = Data<some_type>et toute classe dérivée de Data<some_type> mais permettent également d'autres spécialisations définies ailleurs.

Si je n'avais pas déjà la déclaration du modèle de classe User<>, Je pourrais simplement

template<typename T,
         typename A= typename std::enable_if<is_Data<T>::value>::type>
class User { /*...*/ };

template<template<typename> data>> struct is_Data
{ static const bool value = /* some magic here (not the question) */; };

Cependant, cela a deux paramètres de modèle et se heurte donc à la déclaration précédente, où User<> est déclaré avec un seul paramètre de modèle. Y at-il autre chose que je puisse faire?

(Remarque

template<typename T,
         typename A= typename std::enable_if<is_Data<T>::value>::type>
class User<T> { /*...*/ };

ne fonctionne pas (les arguments de modèle par défaut ne peuvent pas être utilisés dans les spécialisations partielles), ni

template<typename T> class User<Data<T>> { /*...*/ };

car il n'autorise pas les types dérivés de Data<>, ni

template<typename T>
class User<typename std::enable_if<is_Data<T>::value,T>::type>
{ /*...*/ };

puisque le paramètre de modèle T n'est pas utilisé dans la spécialisation partielle.)

41
Walter

Puisque vous avez dit que vous attendiez toujours une meilleure réponse, voici mon point de vue. Ce n'est pas parfait, mais je pense que cela vous amène autant que possible en utilisant SFINAE et des spécialisations partielles. (Je suppose que Concepts fournira une solution complète et élégante, mais nous devrons attendre un peu plus longtemps pour cela.)

La solution repose sur une fonctionnalité des modèles d'alias qui n'a été spécifiée que récemment, dans les brouillons de travail standard après la version finale de C++ 14, mais qui est prise en charge par les implémentations depuis un certain temps. Le libellé pertinent du projet N4527 [14.5.7p3] est:

Cependant, si l'ID de modèle est dépendant, la substitution d'argument de modèle suivante s'applique toujours à l'ID de modèle. [ Exemple:

template<typename...> using void_t = void;
template<typename T> void_t<typename T::foo> f();
f<int>(); // error, int does not have a nested type foo

—End exemple]

Voici un exemple complet de mise en œuvre de cette idée:

#include <iostream>
#include <type_traits>
#include <utility>

template<typename> struct User { static void f() { std::cout << "primary\n"; } };

template<typename> struct Data { };
template<typename T, typename U> struct Derived1 : Data<T*> { };
template<typename> struct Derived2 : Data<double> { };
struct DD : Data<int> { };

template<typename T> void take_data(Data<T>&&);

template<typename T, typename = decltype(take_data(std::declval<T>()))> 
using enable_if_data = T;

template<template<typename...> class TT, typename... Ts> 
struct User<enable_if_data<TT<Ts...>>> 
{ 
    static void f() { std::cout << "partial specialization for Data\n"; } 
};

template<typename> struct Other { };
template<typename T> struct User<Other<T>> 
{ 
    static void f() { std::cout << "partial specialization for Other\n"; } 
};

int main()
{
    User<int>::f();
    User<Data<int>>::f();
    User<Derived1<int, long>>::f();
    User<Derived2<char>>::f();
    User<DD>::f();
    User<Other<int>>::f();
}

L'exécution imprime:

primary
partial specialization for Data
partial specialization for Data
partial specialization for Data
primary
partial specialization for Other

Comme vous pouvez le voir, il y a une ride: la spécialisation partielle n'est pas sélectionnée pour DD, et elle ne peut pas l'être, à cause de la façon dont nous l'avons déclarée. Alors pourquoi ne disons-nous pas

template<typename T> struct User<enable_if_data<T>> 

et lui permettre de correspondre également à DD? Cela fonctionne réellement dans GCC, mais est correctement rejeté par Clang et MSVC à cause de [14.5.5p8.3, 8.4] ([p8.3] peut disparaître dans le futur, car il est redondant - CWG 20 ):

  • La liste d'arguments de la spécialisation ne doit pas être identique à la liste d'arguments implicite du modèle primaire.
  • La spécialisation doit être plus spécialisée que le modèle principal (14.5.5.2).

User<enable_if_data<T>> est équivalent à User<T> (substitution modulo dans cet argument par défaut, qui est traité séparément, comme expliqué par la première citation ci-dessus), donc une forme invalide de spécialisation partielle. Malheureusement, faire correspondre des choses comme DD nécessiterait, en général, un argument de spécialisation partiel de la forme T - il n'y a pas d'autre forme qu'il puisse avoir et il correspond toujours à chaque cas. Donc, je crains que nous puissions dire de manière concluante que cette partie ne peut pas être résolue dans les contraintes données. (Il y a Core issue 198 , qui fait allusion à de futures règles possibles concernant l'utilisation des alias de modèle, mais je doute qu'ils rendront notre cas valide.)

Tant que les classes dérivées de Data<T> sont eux-mêmes des spécialisations de modèle, les contraindre davantage à l'aide de la technique ci-dessus fonctionnera, donc j'espère que cela vous sera d'une certaine utilité.


Prise en charge du compilateur (c'est ce que j'ai testé, d'autres versions peuvent également fonctionner):

  • Clang 3.3 - 3.6.0, avec -Wall -Wextra -std=c++11 -pedantic - fonctionne comme décrit ci-dessus.
  • GCC 4.7.3 - 4.9.2, mêmes options - mêmes que ci-dessus. Curieusement, GCC 5.1.0 - 5.2.0 ne sélectionne plus la spécialisation partielle en utilisant la bonne version du code. Cela ressemble à une régression. Je n'ai pas le temps de rédiger un rapport de bogue approprié; n'hésitez pas à le faire si vous le souhaitez. Le problème semble être lié à l'utilisation de packs de paramètres avec un paramètre de modèle de modèle. Quoi qu'il en soit, GCC accepte la version incorrecte en utilisant enable_if_data<T>, donc cela peut être une solution temporaire.
  • MSVC: Visual C++ 2015, avec /W4, fonctionne comme décrit ci-dessus. Les versions plus anciennes n'aiment pas le decltype dans l'argument par défaut, mais la technique elle-même fonctionne toujours - le remplacement de l'argument par défaut par une autre façon d'exprimer la contrainte le fait fonctionner sur 2013 Update 4.
10
bogdan

[~ # ~] si [~ # ~] la déclaration originale de User<> peut être adapté à

template<typename, typename=std::true_type> class User;

alors on peut trouver une solution (en suivant le commentaire de Luc Danton, au lieu d'utiliser std::enable_if )

template<typename>
struct is_Data : std::false_type {};
template<typename T>
struct is_Data<Data<T>> : std::true_type {};

template<typename T>
class User<T, typename is_Data<T>::type >
{ /* ... */ };

Cependant, ceci ne répond pas à la question d'origine, car il nécessite de changer la définition d'origine de User. Je suis toujours en attente d'une meilleure réponse. Cela pourrait être une solution qui démontre qu'aucune autre solution n'est possible.

25
Walter

Comme vous ne souhaitez l'implémenter que lorsqu'une seule condition est vraie, la solution la plus simple consiste à utiliser une assertion statique. Il ne nécessite pas SFINAE, donne une erreur de compilation claire en cas d'utilisation incorrecte et la déclaration de User<> n'a pas besoin d'être adapté:

template<typename T> class User {
  static_assert(is_Data<T>::value, "T is not (a subclass of) Data<>");
  /** Implementation. **/
};

Voir aussi: Quand utiliser static_assert au lieu de SFINAE? . Le static_assert est une construction c ++ 11, mais il existe de nombreuses solutions de contournement disponibles pour les compilateurs pré-c ++ 11, comme:

#define STATIC_ASSERT(consdition,name) \
  typedef char[(condition)?1:-1] STATIC_ASSERT_ ## name

Si la déclaration de user<> peut être modifié et vous voulez deux implémentations en fonction de la valeur de is_Data, alors il y a aussi une solution qui n'utilise pas SFINAE:

template<typename T, bool D=is_Data<T>::value> class User;

template<typename T> class User<T, true> {
  static_assert(is_Data<T>::value, "T is not (a subclass of) Data<>"); // Optional
  /* Data implementation */
};

template<typename T> class User<T, false> {
  static_assert(!is_Data<T>::value, "T is (a subclass of) Data<>"); // Optional
  /* Non-data implementation */
};

Les assertions statiques vérifient uniquement si l'utilisateur n'a pas spécifié par erreur l'argument du modèle D de manière incorrecte. Si D n'est pas spécifié explicitement, les assertions statiques peuvent être omises.

5
bcmpinc