30 votes

Définition hors ligne de la fonction modèle vs en classe

Je me demandais s'il y avait des avantages de déclarer les modèles de fonction de la ligne de vs dans la classe.

Je vais essayer d'obtenir une compréhension claire des avantages et des inconvénients de les deux de la syntaxe.

Voici un exemple:

Hors de la ligne:

template<typename T>
struct MyType {
    template<typename... Args>
    void test(Args...) const;
};

template<typename T>
template<typename... Args>
void MyType<T>::test(Args... args) const {
    // do things
}

Vs en classe:

template<typename T>
struct MyType {
    template<typename... Args>
    void test(Args... args) const {
        // do things
    }
};

Existe-il des fonctionnalités de langage qui sont plus faciles à utiliser avec la première ou la deuxième version? La première version obtenir de la manière lors de l'utilisation de ce modèle par défaut arguments ou enable_if? Je voudrais voir des comparaisons de la façon dont ces deux cas sont à jouer avec les différentes fonctionnalités de la langue comme sfinae, et peut-être de potentiels futurs fonctionnalités (modules?).

La prise de compilateur comportement spécifique en compte peut être intéressant aussi. Je pense que MSVC besoins en inline dans certains endroits, avec le premier extrait de code, mais je ne suis pas sûr.

EDIT: je sais il n'y a pas de différence sur la façon dont ces caractéristiques fonctionne, que c'est surtout une question de goût. Je veux voir comment les deux syntaxes joue avec différentes techniques, et l'avantage de l'un sur l'autre. Je vois la plupart des réponses qui favorise un sur l'autre, mais je veux vraiment avoir les deux côtés. De façon plus objective, la réponse devrait être mieux.

12voto

user2079303 Points 4916

La séparation de la déclaration de la mise en œuvre permet de le faire:

// file bar.h
// headers required by declaration
#include "foo.h"

// template declaration
template<class T> void bar(foo);

// headers required by the definition
#include "baz.h"

// template definition
template<class T> void bar(foo) {
    baz();
    // ...
}

Maintenant, quel serait-ce utile? Ainsi, l'en-tête baz.h peuvent désormais comporter bar.h et dépendent bar et d'autres déclarations, même si la mise en œuvre de l' bar dépend baz.h.

Si le modèle de fonction a été définie en ligne, il devrait inclure baz.h avant de déclarer bar, et si, baz.h dépend bar, alors vous auriez une dépendance circulaire.


Outre la résolution de dépendances circulaires, de définir des fonctions (que ce soit le modèle ou non) hors-ligne, les feuilles, les déclarations dans un formulaire qui fonctionne efficacement comme une table des matières, ce qui est plus facile pour les programmeurs à lire que les déclarations aspergé dans un en-tête complet de définitions. Cet avantage diminue lorsque vous utilisez des outils de programmation spécialisés qui fournissent un aperçu structuré de l'en-tête.

12voto

Corristo Points 2077

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.

  1. 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
    }
    
  2. 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
    }
    
  3. 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.

  4. 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.

9voto

skypjack Points 5516

Existe-il des fonctionnalités de langage qui sont plus faciles à utiliser avec la première ou la deuxième version?

Tout à fait banale d'un cas, mais il vaut la peine d'être mentionné: les spécialisations.

Comme un exemple, vous pouvez faire cela avec l'extérieur de la ligne de définition:

template<typename T>
struct MyType {
    template<typename... Args>
    void test(Args...) const;

    // Some other functions...
};

template<typename T>
template<typename... Args>
void MyType<T>::test(Args... args) const {
    // do things
}

// Out-of-line definition for all the other functions...

template<>
template<typename... Args>
void MyType<int>::test(Args... args) const {
    // do slightly different things in test
    // and in test only for MyType<int>
}

Si vous voulez faire la même chose avec les définitions de classe seulement, vous devez dupliquer le code pour toutes les autres fonctions de l' MyType "(en supposant test est la seule fonction que vous voulez vous spécialiser, bien sûr).
À titre d'exemple:

template<>
struct MyType<int> {
    template<typename... Args>
    void test(Args...) const {
        // Specialized function
    }

    // Copy-and-paste of all the other functions...
};

Bien sûr, vous pouvez toujours mélanger en-classe et hors-ligne des définitions pour le faire et vous avez la même quantité de code de la pleine hors-ligne version.
De toute façon je suppose que vous êtes orientée vers le plein-dans-classe et plein de ligne de solutions, ainsi mixtes ne sont pas viables.


Une autre chose que vous pouvez faire avec l'extérieur de la ligne des définitions de classe et vous ne pouvez pas faire avec les définitions de classe est fonction des spécialisations de modèle.
Bien sûr, vous pouvez mettre la primaire définition de la catégorie, mais toutes les spécialisations doivent être mis hors-ligne.

Dans ce cas, la réponse à la question est: il existe même des éléments de la langue que vous ne pouvez pas les utiliser avec la version.

Comme exemple, considérons le code suivant:

struct S {
    template<typename>
    void f();
};

template<>
void S::f<int>() {}

int main() {
    S s;
    s.f<int>();
}

Supposons que le concepteur de la classe veut fournir une implémentation pour f seulement pour quelques types spécifiques.
Il ne peut tout simplement pas le faire avec dans-les définitions de classe.


Enfin, des définitions de ligne de les aider à briser les dépendances circulaires.
Cela a été déjà mentionné dans la plupart des autres réponses et il ne prend pas la peine de donner un autre exemple.

1voto

dascandy Points 2818

J'ai tendance à toujours les fusionner - mais vous ne pouvez pas le faire s'ils sont co-dépendant. Régulier de code que vous avez l'habitude de mettre le code dans un .fichier cpp, mais pour les modèles que tout le concept ne s'applique pas (et le fait de répéter les prototypes de fonction). Exemple:

template <typename T>
struct A {
    B<T>* b;
    void f() { b->Check<T>(); }
};

template <typename T>
struct B {
    A<T>* a;
    void g() { a->f(); }
};

Bien sûr c'est un exemple artificiel, mais remplacer les fonctions avec quelque chose d'autre. Ces deux classes de besoin les uns des autres pour être définies avant d'être utilisées. Si vous utilisez une déclaration anticipée de la classe de modèle, vous toujours ne peut pas comprendre la fonction de mise en œuvre de l'un d'eux. C'est une excellente raison pour les mettre hors de la ligne, de 100% des corrections de cette façon à chaque fois.

Une alternative est de faire un de ces intérieur de la classe de l'autre. L'intérieur de la classe peut atteindre l'extérieur de la classe au-delà de sa propre définition pour des fonctions de sorte que le problème est une sorte de caché, qui est utilisable dans la plupart des cas, lorsque vous avez un de ces co-dépendant des classes.

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