2017-10-21 231 views
2

J'ai la classe suivante:Une grande variété de constructeurs sans ambiguïté pour la même classe

class Foo 
{ 
public: 
    // Constructors here 
private: 
    std::vector<X> m_data; // X can be any (unsigned) integer type 
}; 

Je veux le code suivant au travail:

Foo f0; 
Foo f1(1); // and/or f1({1}) 
Foo f2(1, 2); // and/or f2({1, 2}) 
Foo f3(1, 2, 3); // and/or f3({1, 2, 3}) 
Foo f4(1, 2, 3, 4); // ... and so on 
std::vector<int> vec = {1, 2, 3, 4}; 
Foo f5(vec); 
Foo f6(vec.begin(), vec.end()); 
std::list<std::size_t> list = {1, 2, 3, 4}; 
Foo f7(list); 
Foo f8(list.begin(), list.end()); 
std::any_iterable_container container = {1, 2, 3, 4}; 
Foo f9(container); 
Foo f10(container.begin(), container.end()); 
// PS: I guess I only want containers/iterators that store individual 
// values and not pairs (e.g., I don't want care about std::map 
// because it does not make sense anyway). 

Jusqu'à présent, j'ai essayé de combiner SFINAE, constructeur surcharge avec tous les types possibles, modèles variadiques, etc. Chaque fois que je corrige un cas de constructeur, d'autres tombent en panne. De plus, le code que j'écris devient très complexe et difficile à lire. Cependant, le problème semble assez simple et je suppose que je ne fais que l'aborder de manière erronée. Toute suggestion sur la façon d'écrire les constructeurs (idéalement en C++ 17), tout en gardant le code aussi simple que possible, est plus que bienvenue.

Merci.

+0

Je ne suis pas sûr que le problème est aussi simple que vous croyez c'est. –

+0

Pouvez-vous montrer ce que vous avez essayé et ses erreurs? Avec une description de ce que vous voulez travailler –

Répondre

1

En supposant que la classe est définit comme suit:

template <class T> 
class Foo 
{ 
public: 
    [..] 

private: 
    std::vector<T> m_data; 
} 

Brisons cette tâche en sous-tâches:

Construct de itérateurs

template <class Iterator> 
Foo (Iterator begin, Iterator end, typename Iterator::iterator_category * = 0) 
    : m_data(begin, end); 

Nous allons remplir notre m_data à partir de begin et end.

Le troisième paramètre s'assurera que seuls les types Iterator qui déclarent iterator_category correspondent à ce prototype. Puisque cet argument a une valeur par défaut de 0 et n'est jamais spécifié, il sert uniquement à un but pendant le processus de déduction de modèle. Lorsque le compilateur vérifie s'il s'agit du bon prototype, si le type Iterator::iterator_category n'existe pas, il l'ignorera. Puisque iterator_category est un type indispensable pour chaque itérateur standard, il fonctionnera pour eux.

Cette c'tor permettra aux appels suivants:

std::vector<int> vec = {1, 2, 3, 4}; 
Foo<int> f(vec.begin(), vec.end()); 
-- AND -- 
std::list<std::size_t> list = {1, 2, 3, 4}; 
Foo<int> f(list.begin(), list.end()); 

Construct du contenant

template <class Container> 
Foo (const Container & container, decltype(std::begin(container))* = 0, decltype(std::end(container))* = 0) 
    : m_data(std::begin(container), std::end(container)); 

Nous remplirons notre m_data du récipient donné. Nous itérer dessus en utilisant std::begin et std::end, car ils sont plus génériques que leurs homologues .begin() et .end() et supportent plus de types, par ex. tableaux primitifs. Cette c'tor permettra aux appels suivants:

std::vector<int> vec = {1, 2, 3, 4}; 
Foo<int> f(vec); 
-- AND -- 
std::list<std::size_t> list = {1, 2, 3, 4}; 
Foo<int> f(list); 
-- AND -- 
std::array<int,4> arr = {1, 2, 3, 4}; 
Foo<int> f(arr); 
-- AND -- 
int arr[] = {1, 2, 3, 4}; 
Foo<int> f(arr); 

Construct à partir d'une liste initialiseur

template <class X> 
Foo (std::initializer_list<X> && list) 
    : m_data(std::begin(list), std::end(list)); 

Note: Nous prenons la liste comme rvalue référence comme il est généralement le cas , mais nous pourrions également ajouter un Foo (const std::initializer_list<X> & list) pour soutenir la construction de Lvalues.

Nous remplissons notre m_data en itérant de nouveau sur la liste.Et ce c'tor soutiendra:

Foo<int> f1({1}); 
Foo<int> f2({1, 2}); 
Foo<int> f3({1, 2, 3}); 
Foo<int> f4({1, 2, 3, 4}); 

Constructor du nombre variable d'arguments

template <class ... X> 
Foo (X ... args) { 
    int dummy[sizeof...(args)] = { (m_data.push_back(args), 0)... }; 
    static_cast<void>(dummy); 
} 

Ici, remplir les données dans le récipient est un peu plus délicat. Nous utilisons l'expansion des paramètres pour décompresser et pousser chacun des arguments. Cette c'tor nous permet d'appeler:

Foo<int> f1(1); 
Foo<int> f2(1, 2); 
Foo<int> f3(1, 2, 3); 
Foo<int> f4(1, 2, 3, 4); 

classe entière

Le résultat final est très agréable:

template <class T> 
class Foo 
{ 
public: 
    Foo() { 
    std::cout << "Default" << std::endl; 
    } 

    template <class ... X> 
    Foo (X ... args) { 
    int dummy[sizeof...(args)] = { (m_data.push_back(args), 0)... }; 
    static_cast<void>(dummy); 
    std::cout << "VA-args" << std::endl; 
    } 

    template <class X> 
    Foo (std::initializer_list<X> && list) 
     : m_data(std::begin(list), std::end(list)) { 
    std::cout << "Initializer" << std::endl; 
    } 

    template <class Container> 
    Foo (const Container & container, decltype(std::begin(container))* = 0, decltype(std::end(container))* = 0) 
     : m_data(std::begin(container), std::end(container)) { 
    std::cout << "Container" << std::endl; 
    } 

    template <class Iterator> 
    Foo (Iterator first, Iterator last, typename Iterator::iterator_category * = 0) 
     : m_data(first, last) { 
    std::cout << "Iterators" << std::endl; 
    } 

private: 
    std::vector<T> m_data; 
}; 
+0

Merci beaucoup pour votre réponse mon ami. Je dois admettre que la syntaxe du 'Foo (premier, dernier, )' me confond un peu (je ne le comprends pas vraiment). En outre, j'ai réalisé que cela ne fonctionne pas avec 'std :: array' (bien que la question ne demande pas' std :: array'). Est-ce parce que l'itérateur de 'std :: array' est juste un pointeur? Je vous remercie! – AstrOne

+0

@AstrOne, j'ai ajouté quelques informations sur le troisième paramètre, et un exemple pour les tableaux 'std :: array' et primitifs, car cela fonctionne aussi pour eux. –

+0

Merci beaucoup mon ami. Toutes les réponses combinées à quelques lectures supplémentaires m'ont beaucoup aidé à comprendre comment un tel code fonctionne. Bien que toutes les réponses soient assez bonnes j'apprécie vraiment l'aide de tout le monde, j'ai décidé d'accepter votre réponse en raison des détails supplémentaires que vous avez fournis. Je vous remercie. – AstrOne

2

L'idée est de définir la classe comme ceci:

template <typename X> 
class Foo 
{ 
public: 
    Foo() { }; 

    Foo(initializer_list<int> l) :m_data(l) { }; 

    template<typename container> 
    Foo(container const & c) :m_data(c.begin(), c.end()) {}; 

    template<typename iterator> 
    Foo(iterator begin, iterator end) :m_data(begin, end) { }; 
private: 
    std::vector<X> m_data; 
}; 

Où:

Foo() est le constructeur par défaut (non paramétrique).

Foo(initializer_list<int> l) accepte une liste comme {1, 2, 3}.

Foo(container const & c) accepte tout conteneur qui prend en charge begin et end itérateurs. Initialise la classe avec begin et end.

Utilisation:

Foo<int> f0; 
    Foo<int> f1({1}); 
    Foo<int> f2({1, 2}); 
    Foo<int> f3({1, 2, 3}); 
    Foo<int> f4({1, 2, 3, 4}); 
    std::vector<int> vec = {1, 2, 3, 4}; 
    Foo<int> f5(vec); 
    Foo<int> f6(vec.begin(), vec.end()); 
    std::list<size_t> list = {1, 2, 3, 4}; 
    Foo<size_t> f7(list); 
    Foo<size_t> f8(list.begin(), list.end()); 
    set<unsigned> container = {1, 2, 3, 4}; 
    Foo<unsigned> f9(container); 
    Foo<unsigned> f10(container.begin(), container.end()); 
+0

S'il vous plaît expliquer ce que votre code fait un peu plus. – Wndrr

3

La façon la plus simple à mettre en œuvre f1-f4 (qui semblent prendre un nombre variable d'arguments d'un type connu T qui n'est pas un récipient ou iterator) ist ceci:

template<typename... Args> 
Foo(T arg, Args... args) {...} 

Comme ce constructeur prend au moins 1 argument, il n'y a pas d'ambiguïté avec le constructeur par défaut f0. Comme le premier argument est de type T, il n'y a pas d'ambiguïté avec les constructeurs suivants.

Si vous voulez traiter std::vector et std::list différemment que d'autres conteneurs, vous pouvez créer un modèle d'aide en partie spécialisée pour vérifier si un argument est une instance d'un modèle donné:

template<typename> 
struct is_vector : std::false_type {}; 

template<typename T, typename Allocator> 
struct is_vector<std::vector<T, Allocator>> : std::true_type {}; 

Et l'utiliser comme ceci à mettre en œuvre f5 et f7:

template<typename T, Constraint = typename std::enable_if<is_vector<typename std::remove_reference<T>::type>::value, void>::type> 
Foo(T arg) {...} 

en testant les types iterator respectifs de std::vector et std::list ans vous pouvez mettre en œuvre f6 et f8 de la même manière.

Vous pouvez vérifier la présence de fonctions membres begin() et end() à mettre en œuvre f9 (je suppose) comme ceci:

template<typename T> 
Foo(T arg, decltype(arg.begin())* = 0, decltype(arg.end())* = 0) {...} 

Cependant, vous devrez désactiver explicitement ce constructeur pour std::vector et std::list en utilisant la modèles d'aide que vous avez créés pour éviter toute ambiguïté.

Pour vérifier si un argument est un peu iterator pour mettre en œuvre f10, vous pouvez utiliser std::iterator_traits:

template<typename T, typename Constraint = typename std::iterator_traits<T>::iterator_category> 
Foo(T begin, T end) {...} 

Encore une fois, vous devrez désactiver explicitement ce constructeur pour les types iterator de std::vector et std::list.

+0

Merci beaucoup pour votre réponse mon ami. Si je ne définis que le constructeur avec la signature 'template <..stuff..> Foo (T begin, T end), le code suivant échouera:' Foo (1, 2) '. Le compilateur donnera une erreur supplémentaire (en plus de celle attendue): 'no type nommé 'iterator_category' dans 'struct std :: iterator_traits '. Est-ce parce que les 'std :: iterator_traits' ne sont pas SFINAE-friendy? – AstrOne

+0

@AstrOne Je ne suis pas sûr de comprendre. Le constructeur 'Foo (T begin, T end)' est explicitement conçu pour ne prendre que les itérateurs. Il est supposé échouer pour le type 'int'. L'erreur supplémentaire que vous obtenez explique simplement pourquoi le compilateur n'a pas pu appeler ce constructeur candidat avec deux arguments de type 'int', c'est-à-dire parce que' std :: iterator_traits' ne fonctionne bien sûr pas avec 'int' (ce qui est le point). – Knoep