C++11 removed the requirement that the value type of all containers be CopyConstructible and Assignable (although specific operations on containers may impose these requirements). In theory, that should make it possible to define, for example, std::deque<const Foo>
, which was not possible in C++03.
Unexpectedly, gcc 4.7.2 produced its usual vomit of incomprehensible errors [1] when I tried this, but clang at least made the error readable and clang with libc++ compiled it with no errors.
Now, when two different compilers produce different results, it always makes me wonder what the correct answer is, and so I searched out all the references I could find to const/assignable/value types/containers, etc., etc. I found almost a decade's worth of very similar questions and answers, some of them here on SO and others in the various C++ mailing lists, amongst other places, including the Gnu buganizer, all of which basically can be summarized as following dialogue.
Q: Why can't I declare std::vector<const int>
(as a simplified example)
A: Why on earth would you want to do that? It's nonsensical.
Q: Well, it makes sense to me. Why can't I do it?
A: Because the Standard requires value types to be assignable.
Q: But I'm not planning on assigning them. I want them to be const after I've created them.
A: That's not the way it works. Next question!
with a mild dashing of:
A2: C++11 has decided to allow that. You'll just have to wait. In the meantime, rethink your ridiculous design.
These don't seem like very compelling answers, although possibly I'm biased because I fall into the category of "but it makes sense to me". In my case, I'd like to have a stack-like container in which things pushed onto the stack are immutable until they are popped, which doesn't strike me as a particularly odd thing to want to be able to express with a type system.
Anyway, I started thinking about the answer, "the standard requires all containers' value types to be assignable." And, as far as I can see, now that I found an old copy of a draft of the C++03 standard, that's true; it did.
On the other hand, the value type of std::map
is std::pair<const Key, T>
which doesn't look to me like it's assignable. Still, I tried again with std::deque<std::tuple<const Foo>>
, and gcc proceeded to compile it without batting an eye. So at least I have some kind of workaround.
Then I tried printing out the value of std::is_assignable<const Foo, const Foo>
and std::is_assignable<std::tuple<const Foo>, const std::tuple<const Foo>>
, and it turns out that the former is reported as not assignable, as you'd expect, but the latter is reported as assignable (by both clang and gcc). Of course, it's not really assignable; attempting to compile a = b;
is rejected by gcc with the complaint error: assignment of read-only location
(this was just about the only error message I encountered in this quest which was actually easy to understand). However, without the attempt to do an assignment, both clang and gcc are equally happy to instantiate the deque<const>
, and the code seems to run fine.
Now, if std::tuple<const int>
really is assignable, then I can't complain about the inconsistency in the C++03
standard -- and, really, who cares -- but I find it disturbing that two different standard library implementations report that a type is assignable when in fact, attempting to assign to a reference of it will lead to a (very sensible) compiler error. I might at some point want to use the test in a template SFINAE, and based on what I saw today, it doesn't look very reliable.
So is there anyone who can shed some light on the question (in the title): What does Assignable really mean? And two bonus questions:
1) Did the committee really mean to allow instantiating containers with const
value types, or did they have some other non-assignable case in mind?, and
2) Is there really a significant difference between the constnesses of const Foo
and std::tuple<const Foo>
?
[1] For the truly curious, here's the error message produced by gcc when trying to compile the declaration of std::deque<const std::string>
(with a few line-endings added, and an explanation if you scroll down far enough):
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>&]’
So what's going on here is that the standard (§ 20.6.9.1) insists that the default allocator have member functions:
pointer address(reference x) const noexcept;
const_pointer address(const_reference x) const noexcept;
but if you instantiate it with a const
template argument (which is apparently UB), then reference
and const_reference
are the same type, and so the declarations are duplicated. (The body of the definition is identical, for what it's worth.) Consequently, no allocator-aware container can deal with an explicitly const
value type. Hiding the const
inside a tuple
allows the allocator to instantiate. This allocator requirement from the standard was used to justify closing at least a couple of old libstdc++ bugs about problems with std::vector<const int>
, although it doesn't strike me as a solid point of principle. Also libc++ works around the problem in the obvious simple way, which is to provide a specialization of allocator<const T>
with the duplicate function declarations removed.
1 : to transfer (property) to another especially in trust or for the benefit of creditors. 2a : to appoint to a post or duty assigned them to light duty assigned me two clerks. b : to appoint as a duty or task assigns 20 pages for homework.
Assign is the act of transferring rights, property, or other benefits to another party (the assignee) from the party who holds such benefits under contract (the assignor). This concept is used in both contract and property law.
Some common synonyms of assign are ascribe, attribute, credit, and impute. While all these words mean "to lay something to the account of a person or thing," assign implies ascribing with certainty or after deliberation.
Non-assignable agreement means any contract that is assigned to Buyer under this Agreement, but in respect of which an assignment or attempted assignment would constitute a violation of applicable law or in violation of applicable law.
In C++03, Assignable
was defined by table 64 in §23.1/4,
Expression Return type Post-condition t = u T& t is equivalent to u
On the one hand this requirement was not met for std::map
. On the other hand it was too strict a requirement for std::list
. And C++11 demonstrated that it's not even necessary for std::vector
, in general, but is imposed by use of certain operations (such as assignment).
In C++11 the corresponding requirement is named CopyAssignable
and is defined by table 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
The main differences are that container elements no longer need to be CopyAssignable
, and that there's a corresponding requirement MoveAssignable
.
Anyway, a structure with a const
data member is clearly not assignable unless one chooses to read “equivalent to” with a very peculiar interpretation.
The only operation-independent element type requirement in C++11 is, as far as I can see (from table 96 in §23.2.1/4) that it must be Destructible
.
Regarding std::is_assignable
, it does not quite test the CopyAssignable
criterion.
Here’s what std::is_assignable<T, U>
implies, according to table 49 in C++11 §20.9.4.3/3:
“The expression
declval<T>() = declval<U>()
is well-formed when treated as an unevaluated operand (Clause 5). Access checking is performed as if in a context unrelated toT
andU
. Only the validity of the immediate context of the assignment expression is considered. [Note: The compilation of the expression can result in side effects such as the instantiation of class template specializations and function template specializations, the generation of implicitly-defined functions, and so on. Such side effects are not in the “immediate context” and can result in the program being ill-formed. —end note ]”
Essentially this implies an access/existence + argument type compatibility check of operator=
, and nothing more.
However, Visual C++ 11.0 doesn't appear to do the access check, while g++ 4.7.1 chokes on it:
#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.
}
Building with Visual C++ 11.0:
[D:\dev\test\so\assignable] > cl assignable.cpp assignable.cpp [D:\dev\test\so\assignable] > _
Building with 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] > _
So, summing up, the standard's std::is_assignable
appears to be of very limited utility, and as of this writing it can't be relied on for portable code.
EDIT: Replaced <utility>
with correct <type_traits
. Interestingly it didn't matter for g++. Not even for the error message, so I just let that be as it was.
I'm giving this to Alf but I wanted to add a couple of notes for future reference.
As Alf says, std::is_*_assignable
really only check for the existence (explicit or implicit) of an appropriate assignment operator. They don't necessarily check to see whether it will be well-formed if instantiated. This works fine for the default assignment operators. If there is a member declared const
, the default assignment operators will be deleted. If a base class has a deleted assignment operator, the default assignment operator will be deleted. So if you just let the defaults do their thing, it should be fine.
However, if you do declare operator=
, it becomes your responsibility (if you care) to ensure that it is appropriately deleted. For example, this will compile and run (at least with clang), and reports that 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;
}
The absence of the assignment operator's definition isn't noted until link-time, and in this case not at all because the assignment operator is never used. But note that a use of C::operator=
contingent on std::is_assignable
would be allowed to compile. Of course, I couldn't have defined C::operator=
in a way that resulted in assigning to its member a
, because that member isn't assignable.
That's not a particularly interesting example though. What get's interesting is the use of templates, such as the std::tuple
issue which started this whole question. Let's add a couple of templates into the above, and actually define C::operator=
through assignment to its member 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;
}
Without the assignment at the end, the code compiles and runs (under clang 3.3) and reports that A<false>
is not assignable (correct) but that C<false>
is assignable (surprise!). The actual attempt to use C::operator=
reveals the truth, because it is not until that point that the compiler attempts to actually instantiate that operator. Up to then, and through the instantiations of is_assignable
, the operator was just a declaration of a public interface, which is -- as Alf says -- all that std::is_assignable
is really looking for.
Phew.
So bottom line, I think this is a deficiency in both the standard and the standard library implementations with respect to standard aggregate objects whose operator=
should be deleted if any of the component types is not assignable. For std::tuple
, § 20.4.2.2 lists as requirements for operator=
that all component types be assignable, and there ae similar requirements for other types, but I don't think that requirement requires library implementors to delete inapplicable operator=
.
But, then, as far as I can see, nothing stops library implementations from doing the deletion (except the annoyance factor of conditionally deleting assignment operators). In my opinion after obsessing about this for a couple of days, they should do so, and furthermore the standard should require them to do so. Otherwise, reliable use of is_assignable
impossible.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With