Il n'y a pas de différence entre les deux versions concernant le modèle par défaut arguments, SFINAE ou std::enable_if
comme résolution de surcharge et de la substitution des arguments de modèle de travail de la même façon pour chacun d'eux. Je ne vois pas pourquoi il devrait y avoir une différence avec les modules, qu'ils ne changent pas le fait que le compilateur a besoin de voir la définition complète des fonctions de membre de toute façon.
La lisibilité
Un avantage majeur de la sortie de la version en ligne est la lisibilité. Vous pouvez simplement déclarer et de documenter les fonctions de membre et même déplacer les définitions dans un fichier séparé qui est inclus à la fin. Ce qu'il fait en sorte que le lecteur de votre modèle de classe n'a pas à sauter par-dessus un large nombre de détails de mise en œuvre et peut simplement lire le résumé.
Pour votre exemple, vous pourriez avoir les définitions
template<typename T>
template<typename... Args>
void MyType<T>::test(Args... args) const {
// do things
}
dans un fichier appelé" MyType_impl.h
et ensuite le fichier MyType.h
contenir une déclaration
template<typename T>
struct MyType {
template<typename... Args>
void test(Args...) const;
};
#include "MyType_impl.h"
Si MyType.h
contient suffisamment de documentation des fonctions d' MyType
la plupart du temps, les utilisateurs de cette classe n'ont pas besoin de regarder dans les définitions en MyType_impl.h
.
L'expressivité
Mais ce n'est pas seulement augmenté de lisibilité qui différencie hors-ligne et en-les définitions de classe. Alors que tous dans la définition de la classe peut facilement être déplacé à l'extérieur de la ligne de définition, l'inverse n'est pas vrai. I. e. hors-ligne les définitions sont plus expressifs que les définitions de classe. Cela se produit lorsque vous avez étroitement couplée à des classes qui dépendent de la fonctionnalité de chaque autre, de sorte qu'une déclaration anticipée ne suffit pas.
Tel est le cas par exemple du modèle de commande si vous le souhaitez à l'appui de chaîner des commandes et de l'avoir de support de fonctions définies par l'utilisateur et les foncteurs sans avoir à hériter de certains de la classe de base. Donc, un tel Command
est essentiellement une version "améliorée" de la std::function
.
Cela signifie que l' Command
de la classe a besoin d'une forme de type erasure qui je vais omettre ici, mais je peux l'ajouter si quelqu'un voudrais vraiment m'intégrer.
template <typename T, typename R> // T is the input type, R is the return type
class Command {
public:
template <typename U>
Command(U const&); // type erasing constructor, SFINAE omitted here
Command(Command<T, R> const&) // copy constructor that makes a deep copy of the unique_ptr
template <typename U>
Command<T, U> then(Command<R, U> next); // chaining two commands
R operator()(T const&); // function call operator to execute command
private:
class concept_t; // abstract type erasure class, omitted
template <typename U>
class model_t : public concept_t; // concrete type erasure class for type U, omitted
std::unique_ptr<concept_t> _impl;
};
Alors, comment voulez-vous mettre en oeuvre .then
? Le plus simple est de demander à un assistant de classe qui stocke l'original Command
et de la Command
à exécuter suite à cela, et appelle à la fois de leurs opérateurs d'appel, dans l'ordre:
template <typename T, typename R, typename U>
class CommandThenHelper {
public:
CommandThenHelper(Command<T,R>, Command<R,U>);
U operator() (T const& val) {
return _snd(_fst(val));
}
private:
Command<T, R> _fst;
Command<R, U> _snd;
};
Notez que la Commande ne peut pas être un type incomplète au point de cette définition, que le compilateur a besoin de savoir qu' Command<T,R>
et Command<R, U>
mettre en œuvre un appel de l'opérateur ainsi que de leur taille, de sorte que l'avant d'une déclaration n'est pas suffisante ici. Même si vous étiez à stocker les membres de commandes par pointeur, pour la définition de l' operator()
vous avez absolument besoin de l'intégralité de la déclaration de l' Command
.
Avec cette aide, nous pouvons mettre en oeuvre Command<T,R>::then
:
template <typename T, R>
template <typename U>
Command<T, U> Command<T,R>::then(Command<R, U> next) {
// this will implicitly invoke the type erasure constructor of Command<T, U>
return CommandNextHelper<T, R, U>(*this, next);
}
Encore une fois, notez que cela ne fonctionne pas si CommandNextHelper
seulement de l'avant déclaré, parce que le compilateur a besoin de savoir la déclaration du constructeur de CommandNextHelper
. Puisque nous savons déjà que la déclaration de classe de l' Command
a venir avant la déclaration de la CommandNextHelper
, cela signifie que vous simplement ne peut pas définir l' .then
de la fonction dans la classe. La définition de la il a à venir après la déclaration d' CommandNextHelper
.
Je sais que ce n'est pas un exemple simple, mais je ne pouvais pas penser à une, plus simple, parce que le problème provient surtout lorsque vous devez absolument définir un opérateur en tant que membre de la classe. Cela s'applique principalement à l' operator()
et operator[]
dans l'expression des modèles depuis ces opérateurs ne peuvent pas être définis comme des non-membres.
Conclusion
Donc pour conclure: C'est surtout une question de goût que celle que vous préférez, comme il n'y a pas beaucoup de différence entre les deux. Seulement si vous avez des dépendances circulaires entre les cours, vous ne pouvez pas utiliser en classe sur la définition de toutes les fonctions de membre. Personnellement, je préfère le hors-ligne définitions de toute façon, depuis l'astuce de sous-traiter les déclarations de fonction peut également aider avec la génération de la documentation des outils tels que doxygen, qui n'aura alors qu'à créer de la documentation pour la classe proprement dite et non pas pour les aides supplémentaires qui sont définies et déclarées dans un autre fichier.
Modifier
Si je comprends bien votre montage à l'original correctement à la question, vous aimeriez voir comment le général SFINAE, std::enable_if
par défaut et les paramètres du modèle ressemble pour les deux variantes. Les déclarations de regarder exactement la même, seulement pour les définitions que vous avez à déposer les paramètres par défaut si il y a de tout.
-
Par défaut paramètres du modèle
template <typename T = int>
class A {
template <typename U = void*>
void someFunction(U val) {
// do something
}
};
vs
template <typename T = int>
class A {
template <typename U = void*>
void someFunction(U val);
};
template <typename T>
template <typename U>
void A<T>::someFunction(U val) {
// do something
}
-
enable_if
dans le modèle par défaut du paramètre
template <typename T>
class A {
template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
bool someFunction(U const& val) {
// do some stuff here
}
};
vs
template <typename T>
class A {
template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
bool someFunction(U const& val);
};
template <typename T>
template <typename U, typename> // note the missing default here
bool A<T>::someFunction(U const& val) {
// do some stuff here
}
-
enable_if
non-type de paramètre du modèle
template <typename T>
class A {
template <typename U, std::enable_if_t<std::is_convertible<U, T>::value, int> = 0>
bool someFunction(U const& val) {
// do some stuff here
}
};
vs
template <typename T>
class A {
template <typename U, std::enable_if_t<std::is_convertible<U, T>::value, int> = 0>
bool someFunction(U const& val);
};
template <typename T>
template <typename U, std::enable_if_t<std::is_convertible<U, T>::value, int>>
bool A<T>::someFunction(U const& val) {
// do some stuff here
}
Encore une fois, il manque juste le paramètre par défaut de 0.
-
SFINAE dans le type de retour
template <typename T>
class A {
template <typename U>
decltype(foo(std::declval<U>())) someFunction(U val) {
// do something
}
template <typename U>
decltype(bar(std::declval<U>())) someFunction(U val) {
// do something else
}
};
vs
template <typename T>
class A {
template <typename U>
decltype(foo(std::declval<U>())) someFunction(U val);
template <typename U>
decltype(bar(std::declval<U>())) someFunction(U val);
};
template <typename T>
template <typename U>
decltype(foo(std::declval<U>())) A<T>::someFunction(U val) {
// do something
}
template <typename T>
template <typename U>
decltype(bar(std::declval<U>())) A<T>::someFunction(U val) {
// do something else
}
Cette fois, puisqu'il n'existe pas de valeur par défaut paramètres, la déclaration et la définition en fait de la même manière.