2015-07-30 20 views
15

Sto provando a ripetere su un numero di std::list s, ordinando ciascuno di essi. Questo è l'approccio ingenuo:Basato su intervallo per con inizializzatore di controvento su valori non constati?

#include<list> 
using namespace std; 
int main(void){ 
    list<int> a,b,c; 
    for(auto& l:{a,b,c}) l.sort(); 
} 

produrre

aa.cpp:5:25: error: no matching member function for call to 'sort' 
     for(auto& l:{a,b,c}) l.sort(); 
          ~~^~~~ 
/usr/bin/../lib64/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/bits/stl_list.h:1586:7: note: 
     candidate function not viable: 'this' argument has type 'const 
     std::list<int, std::allocator<int> >', but method is not marked const 
     sort(); 
    ^
/usr/bin/../lib64/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/bits/stl_list.h:1596:9: note: 
     candidate function template not viable: requires 1 argument, but 0 were 
     provided 
     sort(_StrictWeakOrdering); 
     ^
1 error generated. 

Sto correttamente indovinando che brace-inizializzatore è la creazione di copie di tali elenchi? E c'è un modo per non copiarli e renderli modificabili all'interno del ciclo? (oltre a fare una lista di puntatori a loro, che è la mia soluzione attuale).

+1

un po 'brutto, ma forse 'per (auto l: {& a, & b, e c}) l-> sort();' –

+0

@Quentin, e l'initializer_list è una lista di copie dell'originale ' a', b', '' C'. Quindi, anche se potessimo ottenere riferimenti non "convenzionali", non sarebbe molto utile. Abbiamo bisogno di un initializer_list (o contenitore simile) che memorizza i riferimenti. Penso che, a meno che tu non costruisca il tuo contenitore (o ne usi uno da Boost) –

risposta

17

Stai supponendo correttamente. Gli elementi std::initializer_list sono sempre const (il che li rende impossibili come sort(), poiché sort() è una funzione membro non const) e i suoi elementi vengono sempre copiati (il che renderebbe sort() -li senza significato anche se non erano const). Da [dcl.init.list], sottolineatura mia:

Un oggetto di tipo std::initializer_list<E> è costruito da una lista di inizializzazione come se l'attuazione assegnato un array temporaneo di N elementi di tipo const E, dove N è il numero di elementi nell'elenco di inizializzazione . Ogni elemento di tale array è inizializzato dalla copia con l'elemento corrispondente dell'elenco di inizializzazione e l'oggetto std::initializer_list<E> viene costruito per fare riferimento a tale array. [Nota: Un costruttore o una funzione di conversione selezionata per la copia deve essere accessibile (Clausola 11) nel contesto della lista di inizializzazione . -end note] Se è richiesta una conversione per il restringimento per inizializzare uno qualsiasi degli elementi, il programma è non formato. [Esempio:

struct X { 
    X(std::initializer_list<double> v); 
}; 
X x{ 1,2,3 }; 

L'inizializzazione sarà attuata in modo meno equivalente a questo:

const double __a[3] = {double{1}, double{2}, double{3}}; 
X x(std::initializer_list<double>(__a, __a+3)); 

supponendo che l'attuazione può costruire un oggetto initializer_list con un paio di puntatori. -end esempio]

Non v'è alcun modo per farli non const o non copiato. La soluzione puntatore funziona:

for (auto l : {&a, &b, &c}) l->sort(); 

perché è il puntatoreche è const, non è l'elemento che sta puntando a.L'altra alternativa sarebbe quella di scrivere un modello di funzione variadic:

template <typename... Lists> 
void sortAll(Lists&&... lists) { 
    using expander = int[]; 
    expander{0, (void(lists.sort()), 0)...}; 
} 

sortAll(a, b, c); 

Si potrebbe anche, immagino, scrivere un aiutante per avvolgere le vostre liste in una matrice di reference_wrapper-list<int> (dal momento che non si può avere una vasta gamma di riferimenti), ma questo è probabilmente più confuso di utile:

template <typename List, typename... Lists> 
std::array<std::reference_wrapper<List>, sizeof...(Lists) + 1> 
as_array(List& x, Lists&... xs) { 
    return {x, xs...}; 
} 

for (list<int>& l : as_array(a, b, c)) { // can't use auto, that deduces 
    l.sort();        // reference_wrapper<list<int>>, 
}           // so would need l.get().sort() 
+1

Nella risposta dovrebbe essere reso più chiaro che l'errore del compilatore è causato dall'implicito 'const', e che la copia del elenchi è un problema aggiuntivo che non ha nulla a che fare con l'errore del compilatore. – vog

+0

@ vog, in un certo senso la copia è in realtà il vero problema con questo codice. Affinché 'sort' si applichi all'originale' a', 'b' e' c' abbiamo bisogno di riferimenti (non-'const') agli oggetti originali. Forse potremmo dire che questo è il motivo per cui i membri sono costanti, per evitare di dare l'impressione che la loro modifica sarebbe utile. –

+1

@AaronMcDaid Concordo pienamente. Sto solo dicendo che la risposta dovrebbe distinguere tra il problema reale e il problema che il compilatore si lamenta, anche se questi due sono correlati. – vog

2

La sintassi {...} è in realtà la creazione di un std::initializer_list. Come pagina stati legati:

Un oggetto std::initializer_list è costruito automaticamente quando:

  • [...]
  • un rinforzato-init-list è destinata a auto, compreso in un spaziato per il ciclo

E:

Un oggetto di tipo std::initializer_list<T> è un oggetto proxy leggero che consente l'accesso a una matrice di oggetti di tipo const T.

Pertanto, non è possibile modificare gli oggetti a cui si accede tramite questo initialize_list. Le tue soluzioni con i puntatori sono le più facili per me.

+0

Altrove su questa domanda hai detto che l'elenco di inizializzazione non copia i suoi elementi. Ma ho controllato questo esempio e l'indirizzo degli articoli nel range-based per è diverso dagli indirizzi dell'originale 'a',' b', 'c' –

+0

@AaronMcDaid sì, ho modificato lo stesso commento di L'ho trovato solo dopo. "L'array sottostante è un array temporaneo, in cui ogni elemento è inizializzato in copia (eccetto che le conversioni di restringimento non sono valide) dall'elemento corrispondente dell'elenco di inizializzazione originale." – Quentin

2

risposta diretta alla sua domanda:

sono io correttamente indovinando che brace-inizializzatore è la creazione di copia di tali elenchi?

Sì, questo è il primo problema. Il tuo codice creerebbe copie dei tuoi elenchi, ordinerà quelle copie e infine dimenticherà le copie ordinate.

Tuttavia,, questo da solo comporterebbe un codice non funzionante. L'errore del compilatore suggerisce un secondo problema: il tipo implicito di l è list<int> const& anziché list<int>&. Quindi il compilatore si lamenta che sort() prova a modificare gli elenchi costanti.

È possibile aggirare questo secondo problema con un brutto const_cast:

#include <list> 
#include <iostream> 
using namespace std; 
int main(void){ 
    list<int> a,b,c; 
    a.push_back(2); 
    a.push_back(0); 
    a.push_back(1); 
    for(auto& l:{a,b,c}) const_cast<list<int>&>(l).sort(); 
    for(auto i:a) cout << i << endl; 
} 

tuttavia, che poi innescare il primo problema: Il tuo elenco delle liste contiene copie, e solo quelle copie sono allineati.Così il risultato finale non è quello che si vuole:

2 
0 
1 

La soluzione più semplice è quello di creare una lista di puntatori agli elenchi:

#include <list> 
#include <iostream> 
using namespace std; 
int main(void){ 
    list<int> a,b,c; 
    a.push_back(2); 
    a.push_back(0); 
    a.push_back(1); 
    for(auto l:{&a,&b,&c}) l->sort(); 
    for(auto i:a) cout << i << endl; 
} 

questo produrrà il risultato desiderato:

0 
1 
2 
6

È possibile scrivere una funzione ref_range che consente di effettuare questa operazione:

for(auto& l : ref_range(a,b,c)) { 
    l.sort(); 
} 

Come altri hanno già detto, una volta che scrivi {a,b,c} sei bloccato con un initializer_list, e tale elenco prende sempre copie dei suoi argomenti. Le copie sono const (quindi il tuo errore), ma anche se potresti ottenere un riferimento non const, modificheresti le copie di a, b e c invece degli originali.

Ad ogni modo, ecco ref_range. Costruisce un vector di reference_wrapper.

// http://stackoverflow.com/questions/31724863/range-based-for-with-brace-initializer-over-non-const-values 
#include<list> 
#include<functional> 
#include<array> 

template<typename T, std:: size_t N> 
struct hold_array_of_refs { 
    using vec_type = std:: array< std:: reference_wrapper<T>, N >; 
    vec_type m_v_of_refs; 
    hold_array_of_refs(vec_type && v_of_refs) : m_v_of_refs(std::move(v_of_refs)) { } 
    ~hold_array_of_refs() { } 
    struct iterator { 
     typename vec_type :: const_iterator m_it; 
     iterator(typename vec_type :: const_iterator it) : m_it(it) {} 
     bool operator != (const iterator &other) { 
      return this->m_it != other.m_it; 
     } 
     iterator& operator++() { // prefix 
      ++ this->m_it; 
      return *this; 
     } 
     T& operator*() { 
      return *m_it; 
     } 
    }; 

    iterator begin() const { 
     return iterator(m_v_of_refs.begin()); 
    } 
    iterator end() const { 
     return iterator(m_v_of_refs.end()); 
    } 
}; 

template<typename... Ts> 
using getFirstTypeOfPack = typename std::tuple_element<0, std::tuple<Ts...>>::type; 


template<typename ...T> 
auto ref_range(T&... args) -> hold_array_of_refs< getFirstTypeOfPack<T...> , sizeof...(args)> { 
    return {{{ std:: ref(args)... }}}; // Why does clang prefer three levels of {} ? 
} 

#include<iostream> 
int main(void){ 
    std:: list<int> a,b,c; 
    // print the addresses, so we can verify we're dealing 
    // with the same objects 
    std:: cout << &a << std:: endl; 
    std:: cout << &b << std:: endl; 
    std:: cout << &c << std:: endl; 
    for(auto& l : ref_range(a,b,c)) { 
     std:: cout << &l << std:: endl; 
     l.sort(); 
    } 
} 
+0

Qualche ragione particolare per cui usi 'std :: vector'? Come sapete le dimensioni al momento della compilazione 'può essere usato std :: array' o mi manca qualcosa? – Slava

+0

Hai ragione, lo cambierò ora –

+0

Modificato su 'array', grazie a @Slava. E rif_range semplificato un po '. Ma sono sorpreso un po 'di clang, suggeriva di aggiungere un terzo set di parentesi nella mia dichiarazione di ritorno da 'ref_range' –

Problemi correlati