2012-12-23 15 views
25

C++ 11 ha rimosso il requisito che il tipo di valore di tutti i contenitori sia CopyConstructible e Assignable (sebbene le operazioni specifiche sui contenitori possano imporre tali requisiti). In teoria, ciò dovrebbe consentire di definire, ad esempio, std::deque<const Foo>, cosa che non era possibile in C++ 03.Cosa significa "Assegnabile"?

Inaspettatamente, gcc 4.7.2 ha prodotto il suo solito vomito di errori incomprensibili [1] quando ho provato questo, ma clang almeno ha reso l'errore leggibile e clang con libC++ lo ha compilato senza errori.

Ora, quando due diversi compilatori producono risultati diversi, mi viene sempre da chiedersi qual è la risposta corretta, quindi ho cercato tutti i riferimenti che ho trovato per const/assegnabili/tipi/contenitori di valori, ecc., Ecc. Ho trovato quasi un decennio di domande e risposte molto simili, alcune di queste qui su SO e altre nelle varie mailing list C++, tra le altre cose, incluso il bugan Gnu, che fondamentalmente possono essere riassunte come segue.

D: Perché non posso dichiarare std::vector<const int> (come un esempio semplificato)

A: Perché sulla terra si vuole farlo? È senza senso.

Q: Beh, per me ha senso. Perché non posso farlo?

A: Perché lo standard richiede che i tipi di valore siano assegnabili.

Q: Ma non ho intenzione di assegnarli. Voglio che siano costanti dopo che li ho creati.

A: Questo non è il modo in cui funziona. Prossima domanda!

con un ardito lieve:

A2: C++ 11 ha deciso di permetterlo. Dovrai solo aspettare. Nel frattempo, ripensa il tuo ridicolo design.

Questi non sembrano risposte molto convincenti, anche se forse sono di parte perché rientrano nella categoria di "ma ha senso per me". Nel mio caso, mi piacerebbe avere un contenitore tipo stack in cui le cose spinte in pila siano immutabili finché non vengono spuntate, il che non mi sembra una cosa particolarmente strana da voler essere in grado di esprimere con un tipo sistema.

In ogni caso, ho iniziato a pensare la risposta, "la norma richiede tipi di valore tutti i contenitori per essere assegnabili." E, per quanto posso vedere, ora che ho trovato una vecchia copia di una bozza dello standard C++ 03, è vero; lo ha fatto.

D'altra parte, il tipo di valore di std::map è std::pair<const Key, T> che non mi sembra sia assegnabile. Tuttavia, ho provato di nuovo con std::deque<std::tuple<const Foo>> e gcc ha proceduto alla compilazione senza battere ciglio. Quindi almeno ho una sorta di soluzione alternativa.

Poi ho provato a stampare il valore di std::is_assignable<const Foo, const Foo> e std::is_assignable<std::tuple<const Foo>, const std::tuple<const Foo>>, e si scopre che il primo è segnalato come non assegnabile, come ci si aspetterebbe, ma quest'ultimo è segnalato come assegnabile (da clang e gcc). Certo, non è veramente assegnabile; il tentativo di compilare a = b; viene respinto da gcc con il reclamo error: assignment of read-only location (questo era solo l'unico messaggio di errore che ho incontrato in questa ricerca che era in realtà facile da capire). Tuttavia, senza il tentativo di fare un incarico, sia clang che gcc sono ugualmente felici di istanziare lo deque<const> e il codice sembra funzionare bene.

Ora, se è davvero std::tuple<const int> assegnabile, quindi non posso lamentare l'incoerenza nello standard C++03 - e, in realtà, chi se ne frega - ma trovo inquietante che due diverse implementazioni della libreria standard riferiscono che un tipo è assegnabile quando, infatti, il tentativo di assegnargli un riferimento porterà ad un (molto sensibile) errore del compilatore. Potrei a un certo punto voler utilizzare il test in un modello SFINAE e, in base a ciò che ho visto oggi, non sembra molto affidabile.

Quindi c'è qualcuno che può far luce sulla domanda (nel titolo): Cosa significa Assignable veramente? E due bonus domande:

1) ha fatto il comitato significa veramente per consentire istanziare contenitori con const tipi di valore, o hanno avere qualche altro caso non cedibile in mente ?, e

2) Esiste davvero una differenza significativa tra le costanze di const Foo e std::tuple<const Foo>?


[1] Per i veri curiosi, ecco il messaggio di errore prodotto da gcc quando si cerca di compilare la dichiarazione di std::deque<const std::string> (con qualche linea di terminazioni aggiunto, e una spiegazione se si scorre eccessivamente verso il basso):

In file included from /usr/include/x86_64-linux-gnu/c++/4.7/./bits/c++allocator.h:34:0, 
       from /usr/include/c++/4.7/bits/allocator.h:48, 
       from /usr/include/c++/4.7/string:43, 
       from /usr/include/c++/4.7/random:41, 
       from /usr/include/c++/4.7/bits/stl_algo.h:67, 
       from /usr/include/c++/4.7/algorithm:63, 
       from const_stack.cc:1: 
/usr/include/c++/4.7/ext/new_allocator.h: In instantiation of ‘class __gnu_cxx::new_allocator<const std::basic_string<char> >’: 
/usr/include/c++/4.7/bits/allocator.h:89:11: required from ‘class std::allocator<const std::basic_string<char> >’ 
/usr/include/c++/4.7/bits/stl_deque.h:489:61: required from ‘class std::_Deque_base<const std::basic_string<char>, std::allocator<const std::basic_string<char> > >’ 
/usr/include/c++/4.7/bits/stl_deque.h:728:11: required from ‘class std::deque<const std::basic_string<char> >’ 
const_stack.cc:112:27: required from here 
/usr/include/c++/4.7/ext/new_allocator.h:83:7: 
    error: ‘const _Tp* __gnu_cxx::new_allocator< <template-parameter-1-1> >::address(
    __gnu_cxx::new_allocator< <template-parameter-1-1> >::const_reference) const [ 
    with _Tp = const std::basic_string<char>; 
    __gnu_cxx::new_allocator< <template-parameter-1-1> >::const_pointer = 
    const std::basic_string<char>*; 
    __gnu_cxx::new_allocator< <template-parameter-1-1> >::const_reference = 
    const std::basic_string<char>&]’ cannot be overloaded 
/usr/include/c++/4.7/ext/new_allocator.h:79:7: 
    error: with ‘_Tp* __gnu_cxx::new_allocator< <template-parameter-1-1> >::address(
    __gnu_cxx::new_allocator< <template-parameter-1-1> >::reference) const [ 
    with _Tp = const std::basic_string<char>; 
    __gnu_cxx::new_allocator< <template-parameter-1-1> >::pointer = const std::basic_string<char>*; 
    __gnu_cxx::new_allocator< <template-parameter-1-1> >::reference = const std::basic_string<char>&]’ 

Allora, cosa sta succedendo qui è che lo standard (§ 20.6.9.1) insiste sul fatto che l'allocatore di default hanno funzioni membro:

pointer address(reference x) const noexcept; 
const_pointer address(const_reference x) const noexcept; 

ma se lo si istanzia con un argomento modello const (che è apparentemente UB), quindi reference e const_reference sono dello stesso tipo e quindi le dichiarazioni vengono duplicate. (Il corpo della definizione è identico, per quello che vale.) Di conseguenza, nessun contenitore abilitato per l'allocatore può gestire un tipo di valore esplicitamente const. Nascondere lo const all'interno di uno tuple consente all'allocatore di creare un'istanza. Questo requisito degli allocatori dello standard è stato utilizzato per giustificare la chiusura di almeno un paio di vecchi bug libstdC++ relativi ai problemi con std::vector<const int>, sebbene non mi colpisca come un solido punto di principio. Anche libC++ risolve il problema nel modo ovvio semplice, ovvero fornire una specializzazione di allocator<const T> con le dichiarazioni di funzioni duplicate rimosse.

+2

IIRC il requisito Assignable non viene rimosso, è diviso in due (per copie e per mosse) –

+0

Non hai già capito che il problema non sono i requisiti imposti da 'std :: deque' (o sequenza contenitori in generale), ma per 'std :: allocator'? Piuttosto che _questo significato assegnabile_, la tua domanda non dovrebbe essere corretta se si utilizza un allocatore che funziona con 'const' types_ (tenendo presente che' std :: allocator' non funziona con i tipi 'const')? – jogojapan

+0

@ BenVoigt: è davvero finito. Era il 23.1 (3): "Il tipo di oggetti memorizzati in questi componenti deve soddisfare i requisiti dei tipi CopyConstructible (20.1.3) e i requisiti aggiuntivi dei tipi Assegnabili". Quel paragrafo, il prossimo, e la tabella associata 64, (riferendosi ad Assignable) sono tutti spariti. Il requisito rimanente è che se si utilizza un costruttore di copia o un'assegnazione di copia sul contenitore, il tipo di valore T deve essere CopyInsertable nel tipo di contenitore X. – rici

risposta

12

In C++ 03, Assignable è stato definito da tavolo 64 in §23.1/4,

 
    Expression Return type Post-condition 
    t = u   T&    t is equivalent to u 

Da un lato tale requisito non è stata soddisfatta per std::map. D'altra parte era un requisito troppo severo per std::list. E C++ 11 ha dimostrato che non è nemmeno necessario per std::vector, in generale, ma è imposto dall'uso di determinate operazioni (come l'assegnazione).

In C++ 11 il requisito corrispondente è chiamato CopyAssignable ed è definito dalla tavola 23 in §17.6.3.1/2,

 
    Expression Return type Return value Post-condition 
    t = v   T&    t    t is equivalent to v, the 
               value of v is unchanged 

Le principali differenze sono che elementi contenitori non devono più essere CopyAssignable, e che c'è un corrispondente requisito MoveAssignable.

In ogni caso, una struttura con un membro dati const non è chiaramente assegnabile a meno che non si scelga di leggere “ equivalente a ” con un'interpretazione molto particolare.

L'unico requisito del tipo di elemento indipendente dall'operazione in C++ 11 è, per quanto posso vedere (dalla tabella 96 in §23.2.1/4) che deve essere Destructible.


Per quanto riguarda std::is_assignable, non del tutto testare il criterio CopyAssignable.

Qui ’ è ciò std::is_assignable<T, U> implica, secondo la tabella 49 in C++ 11 §20.9.4.3/3:

“ L'espressione declval<T>() = declval<U>() è ben formato se trattati come operando non valutata (clausola 5). L'accesso al controllo viene eseguito come se in un contesto non correlato a T e U. Solo la validità di nel contesto immediato di è considerata l'espressione di assegnazione . [Nota: La compilazione del espressione può provocare effetti collaterali, come la istanza di classe specializzazioni template e modello di funzione specializzazioni, la generazione di implicitamente definiti funzioni, e così via. Tali effetti secondari non sono nel "contesto immediato" e può causare il programma in formato non corretto. -end nota] ”

Essenzialmente questo implica un controllo di compatibilità di accesso/esistenza + tipo di argomento di operator=, e nulla più.

Tuttavia, Visual C++ 11.0 non sembra fare il controllo di accesso, mentre g ++ 4.7.1 soffoca su di esso:

#include <iostream> 
#include <type_traits> 
#include <tuple> 
using namespace std; 

struct A {}; 
struct B { private: B& operator=(B const&); }; 

template< class Type > 
bool isAssignable() { return is_assignable< Type, Type >::value; } 

int main() 
{ 
    wcout << boolalpha; 
    wcout << isAssignable<A>() << endl;    // OK. 
    wcout << isAssignable<B>() << endl;    // Uh oh. 
} 

Costruire con Visual C++ 11.0:

 
[D:\dev\test\so\assignable] 
>cl assignable.cpp 
assignable.cpp 

[D:\dev\test\so\assignable] 
> _ 

Costruire con g ++ 4.7.1:

 
[D:\dev\test\so\assignable] 
>g++ assignable.cpp 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In substitution of 'template static decltype (((declval)()=(declval)(), std::__sfinae_types::__one()))std::__is_assignable_helper::__test(int) [with _ 
Tp1 = _Tp1; _Up1 = _Up1; _Tp = B; _Up = B] [with _Tp1 = B; _Up1 = B]': 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68: required from 'constexpr const bool std::__is_assignable_helper::value' 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12: required from 'struct std::is_assignable' 
assignable.cpp:10:59: required from 'bool isAssignable() [with Type = B]' 
assignable.cpp:16:32: required from here 
assignable.cpp:7:24: error: 'B& B::operator=(const B&)' is private 
In file included from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40, 
       from assignable.cpp:1: 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1049:2: error: within this context 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In substitution of 'template static decltype (((declval)()=(declval)(), std::__sfinae_types::__one())) std::__is_assignable_helper::__test(int) [with _ 
Tp1 = _Tp1; _Up1 = _Up1; _Tp = B; _Up = B] [with _Tp1 = B; _Up1 = B]': 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68: required from 'constexpr const bool std::__is_assignable_helper::value' 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12: required from 'struct std::is_assignable' 
assignable.cpp:10:59: required from 'bool isAssignable() [with Type = B]' 
assignable.cpp:16:32: required from here 
assignable.cpp:7:24: error: 'B& B::operator=(const B&)' is private 
In file included from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40, 
       from assignable.cpp:1: 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1049:2: error: within this context 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In instantiation of 'constexpr const bool std::__is_assignable_helper::value': 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12: required from 'struct std::is_assignable' 
assignable.cpp:10:59: required from 'bool isAssignable() [with Type = B]' 
assignable.cpp:16:32: required from here 
assignable.cpp:7:24: error: 'B& B::operator=(const B&)' is private 
In file included from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40, 
       from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40, 
       from assignable.cpp:1: 
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68: error: within this context 

[D:\dev\test\so\assignable] 
> _ 

Quindi, riassumendo, il std::is_assignable dello standard sembra essere di molto li Utilità mitata e al momento della stesura di questo documento non si può fare affidamento sul codice portatile.


EDIT: sostituito <utility> con la corretta <type_traits. È interessante notare che non ha importanza per g ++. Nemmeno per il messaggio di errore, quindi ho lasciato che fosse come era.

+0

Giusto per essere chiari, quindi, sei d'accordo che i bug dovrebbero essere archiviati contro entrambe le librerie standard citate? – rici

+0

@rici: no, non per il motivo di riportare 'true' per la tupla, perché anche se chiaramente non è' CopyAssignable', sembra soddisfare i requisiti di 'std :: is_assignable'. Vedi la seconda parte della mia risposta (l'ho appena aggiunto). Tuttavia, almeno * alcune * implementazioni sono buggy, dal momento che non sono d'accordo! –

+2

FWIW il tuo programma è accettato con un'istantanea del prossimo GCC 4.8, producendo risultati attesi (cioè emettendo 'true' e 'false'). –

4

sto dando questo per Alf ma ho voluto aggiungere un paio di note per riferimento futuro.

Come Alf dice, std::is_*_assignable controlla solo l'esistenza (esplicita o implicita) di un operatore di assegnazione appropriato. Non controllano necessariamente per vedere se sarà ben formato se istanziato. Funziona bene per gli operatori di assegnazione predefiniti. Se v'è un membro dichiarato const, gli operatori di assegnazione di default verranno eliminati. Se una classe base ha un operatore di assegnazione eliminato, l'operatore di assegnazione predefinito verrà cancellato. Quindi se lasci che le impostazioni predefinite facciano le loro cose, dovrebbe andare bene.

Tuttavia, se si dichiara operator=, diventa responsabilità dell'utente (se si cura) di assicurarsi che sia eliminato in modo appropriato. Ad esempio, ciò compilare ed eseguire (almeno con clangore), e segnala che C is_assignable:

#include <iostream> 
#include <type_traits> 
#include <tuple> 
using namespace std; 

struct A { const int x; A() : x() {}}; 

struct C { 
    struct A a; 
    C& operator=(C const& other); 
}; 

template< class Type > 
bool isAssignable() { return is_assignable< Type&, const Type& >::value; } 

int main() 
{ 
    wcout << boolalpha; 
    wcout << isAssignable<A>() << endl; // false 
    wcout << isAssignable<C>() << endl; // true 
    C c1; 
    C c2; 
} 

L'assenza di definizione dell'operatore di assegnazione non è nota fino link-tempo, e in questo caso non a tutto perché l'operatore di assegnazione non viene mai utilizzato. Ma si noti che l'uso di C::operator= subordinata alla std::is_assignable sarebbe stato permesso di compilazione. Certo, non avrei potuto definita C::operator= in un modo che ha portato ad assegnare ai suoi Stati a, perché il membro non è cedibile.

Questo non è un esempio particolarmente interessante però. La cosa interessante è l'uso di modelli, come il problema std::tuple che ha dato inizio all'intera domanda. Aggiungiamo un paio di modelli in quanto sopra, e in realtà definire C::operator= mediante assegnazione al suo membro a:

using namespace std; 

template<bool> struct A { 
    A() : x() {} 
    const int x; 
}; 

template<bool B> struct C { 
    struct A<B> a; 
    C& operator=(C const& other) { 
    this->a = other.a; 
    return *this; 
    } 
}; 

template< class Type > 
bool isAssignable() { return is_assignable< Type&, const Type& >::value; } 

int main() 
{ 
    wcout << boolalpha; 
    wcout << isAssignable< A<false> >() << endl; // false 
    wcout << isAssignable< C<false> >() << endl; // true 
    C<false> c1; 
    C<false> c2; 
    c1 = c2;          // Bang 
    return 0; 
} 

Senza l'incarico alla fine, il codice compilato ed eseguito (sotto clang 3.3) e le relazioni che A<false> è non assegnabile (corretto), ma che C<false> è assegnabile (sorpresa!). Il vero tentativo di usare C::operator= rivela la verità, perché non è fino a quel punto che il compilatore tenta di istanziare effettivamente quell'operatore. Fino ad allora, e attraverso le istanze di is_assignable, l'operatore era solo una dichiarazione di un'interfaccia pubblica, che è - come dice Alf - tutto ciò che lo std::is_assignable è davvero alla ricerca.

Phew.

linea di fondo Quindi, penso che questo sia una carenza sia nella norma e le implementazioni della libreria standard per quanto riguarda gli oggetti di aggregazione standard cui operator= dovrebbe essere eliminato se uno qualsiasi dei tipi di componenti non è cedibile. Per , § 20.4.2.2 elenca come requisiti per operator= che tutti i tipi di componente siano assegnabili e ci sono requisiti simili per altri tipi, ma non penso che il requisito richieda che gli implementatori di libreria eliminino l'inapplicabile operator=.

Ma, quindi, per quanto posso vedere, nulla impedisce alle implementazioni della libreria di eseguire la cancellazione (tranne il fattore di disturbo dell'eliminazione condizionale degli operatori di assegnazione). Secondo me, dopo esserne ossessionato per un paio di giorni, dovrebbero farlo, e inoltre lo standard dovrebbe richiedere loro di farlo. Altrimenti, è impossibile l'uso affidabile di is_assignable.

+0

@LucDanton: ma nel caso di 'std :: tuple ', l'operatore di assegnazione non ha senso. Penso che 'std :: tuple' dovrebbe cancellarlo esplicitamente. In questo caso, 'std :: is_assignable >' sarebbe 'false_type' che è sicuramente il punto di' is_assignable', oltre a soddisfare POLS. Se tutti gli oggetti basati su modelli la cui assegnabilità dipende dall'assegnabilità di (alcuni) argomenti del modello dovessero eliminare l'operatore di assegnazione nel caso in cui gli argomenti del modello pertinenti non fossero assegnabili, allora l'intero apparecchio funzionerebbe correttamente. – rici

+0

@LucDanton, i margini sono troppo piccoli :). Se intendo testare i parametri del template per l'assegnabilità, funzionerà bene finché lo farà anche chiunque altro. Non posso farlo da solo Inoltre, se cancello correttamente l'operatore di assegnazione per le istanze non assegnabili, il client riceverà un messaggio di errore di facile comprensione (operatore di assegnazione cancellato), piuttosto che dover risalire alle pagine del traceback di istanziazione del modello. – rici

+0

Una considerazione pragmatica sul motivo per cui le tuple non sono obbligate a fornire condizionatamente agli operatori di assegnazione è che è più fastidioso fornire del solito (specializzazioni parziali invece del tipo più diretto di SFINAE). In ogni caso, per quanto riguarda la situazione attuale, non darei la colpa alle implementazioni, ma ai requisiti. –