2015-02-06 10 views
6

Quando le classi hanno un sovraccarico del costruttore che prende un std::initializer_list, questo sovraccarico avrà la precedenza anche se altri sovraccarichi del costruttore sono apparentemente una corrispondenza migliore. Questo problema è descritto in dettaglio in Sutter di GotW#1, parte 2, nonché Meyers' Effective Modern C++, punto 7.Come progettare le classi con il costruttore che prende uno std :: initializer_list?

Il classico esempio di dove questo problema si manifesta è quando brace-inizializzazione un std::vector:

std::vector<int> vec{1, 2}; 
// Is this a vector with elements {1, 2}, or a vector with a single element 2? 

Sia Sutter che Meyers consigliano di evitare progettazioni di classe in cui un sovraccarico del costruttore initializer_list può causare ambiguità al programmatore.

Sutter:

guida: Quando si progetta una classe, evitare di fornire un costruttore che sovraccarichi ambiguamente con un costruttore initializer_list, in modo che gli utenti non dovranno utilizzare() per raggiungere un tale nascosta costruttore.

Meyers:

Di conseguenza, è meglio per progettare i costruttori in modo che il sovraccarico di chiamato non è influenzato dal fatto che i client utilizzano parentesi graffe o . In altre parole, impara da ciò che viene ora visualizzato come un errore in il design dell'interfaccia std :: vector e progetta le tue classi su evitalo.

Ma nessuno di essi descrivono come vector avrebbe dovuto essere progettato per evitare il problema!

Quindi, ecco la mia domanda: Come avrebbe dovuto vector stati progettati per evitare ambiguità con il sovraccarico di costruttore initializer_list (senza perdere alcuna funzionalità)?

risposta

8

lo farei adottare lo stesso approccio adottato dallo standard con piecewise_construct in pair o defer_lock in unique_lock: utilizzando i tag sul costruttore:

struct n_copies_of_t { }; 
constexpr n_copies_of_t n_copies_of{}; 

template <typename T, typename A = std::allocator<T>> 
class vector { 
public: 
    vector(std::initializer_list<T>); 
    vector(n_copies_of_t, size_type, const T& = T(), const A& = A()); 
    // etc. 
}; 

questo modo:

std::vector<int> v{10, 20}; // vector of 2 elems 
std::vector<int> v2(10, 20); // error - not a valid ctor 
std::vector<int> v3(n_copies_of, 10, 20); // 10 elements, all with value 20. 

Inoltre, ho sempre dimenticare se si tratta di 10 elementi di valore 20 o 20 elementi di valore 10, in modo che il tag aiuta a chiarire che .

+0

Fantastico! Non ero a conoscenza del fatto che avessero risolto il problema in altre parti della libreria standard. –

+0

Questo approccio ha il vantaggio che i sovraccarichi del costruttore sono utilizzabili sia per l'allocazione di heap che di stack. –

+0

@EmileCormier: è possibile utilizzare questi sovraccarichi per allocazioni heap e stack in un modo o nell'altro ... no? Non riesco a pensare a nulla che lo possa impedire. –

1

Un possibile modo per evitare l'ambiguità consiste nell'utilizzare metodi di fabbrica statici come mezzo per isolare il costruttore initializer_list dagli altri.

Ad esempio:

template <typename T> 
class Container 
{ 
public: 
    static Container with(size_t count, const T& value) 
    { 
     return Container(Tag{}, count, value); 
    } 

    Container(std::initializer_list<T> list) {/*...*/} 

private: 
    struct Tag{}; 
    Container(Tag, size_t count, const T& value) {/*...*/} 
}; 

Usage:

auto c1 = Container<int>::with(1, 2); // Container with the single element '2' 
auto c2 = Container<int>{1, 2}; // Container with the elements {1, 2} 

Questo approccio fabbrica statica ricorda di come gli oggetti sono allocated and initialized in Objective-C. La struttura nidificata Tag viene utilizzata per garantire che il sovraccarico di initializer_list non sia valido.


In alternativa, il costruttore initializer_list può essere cambiato in un metodo factory statico, che consente di mantenere le altre sovraccarichi costruttore intatta:

template <typename T> 
class Container 
{ 
public: 
    static Container with(std::initializer_list<T> list) 
    { 
     return Container(Tag{}, list); 
    } 

    Container(size_t count, const T& value) {/*...*/} 

private: 
    struct Tag{}; 
    Container(Tag, std::initializer_list<T> list) {/*...*/} 
}; 

Usage:

auto c1 = Container<int>{1, 2}; // Container with the single element '2' 
auto c2 = Container<int>::with({1, 2}); // Container with the elements {1, 2} 
+0

Questo approccio ha lo svantaggio che i sovraccarichi "statici" non possono essere utilizzati per inizializzare un contenitore sull'heap. –

+0

Intendevo dire che i sovraccarichi "static-ified" non possono essere usati per ** direttamente ** inizializzare un contenitore allocato all'heap. Potrei sempre fare questo: 'auto heapContainer = new Container (Container :: with (1,2));' (che potrebbe comportare una leggera penalizzazione delle prestazioni). –