References don’t have top-level cv-qualifiers

Sometimes when reading about C++, for instance about template argument deduction, the term “top-level cv-qualifiers” comes up. I just spent an unreasonable amount of time being puzzled by something because I didn’t understand that references don’t have top-level cv-qualifiers. This post will hopefully help the next person (hi, next-year Anders) not make the same mistake.

Looking at

const int&

, I assumed without even thinking about it that the const here was the top-level one. It is not.

First, what’s a cv-qualifier? It’s const, volatile or both. Let’s just use const as an example for this article.

Then, what’s a top-level cv-qualifier? The best way to explain is with an example, and the best example is a pointer. There are two levels of constness to a pointer, the constness of the pointer itself, and the constness of what it’s pointing to.

Given const int *, a non-const pointer to const int, we can visualise it as

pointer (the * part)
const int (the const int part)

And given const int * const, a const pointer to const int, we can visualise it as

const pointer (the * const part)
const int (the const int part)

(And so on, you can imagine how it looks for pointers to non-const int.) The top-level cv-qualifier is the one on the top level, the cv-qualifier on the pointer itself.

Now, how does this look for references?

Given const int&, a reference to const int, we can visualise it as

reference (the & part)
const int (the const int part)

But there’s no such thing as a const reference! Constness applies to the object itself, and a reference is not an object, just an alternative name for an existing object. So there is no such thing as a const int& const, i.e. there’s no such thing as

const reference (the & const part)
const int (the const int part)

Which means, references don’t have top-level cv-qualifiers. The standard even has an example:

Example: The type corresponding to the type-id const int& has no top-level cv-qualifiers.


This is by the way a somewhat recent addition, until this Core Language Defect Report was resolved in 2014, the term “top-level cv-qualifier” was never actually defined in the standard.

Why we probably shouldn’t have constexpr conditional operator

The idea

I had a great idea. We have constexpr if, but no constexpr conditional operator. Time for a proposal?

Since we can do stuff like this:

if constexpr(cond) { foo; } else { bar;}

Wouldn’t it be cool if we could also do

cond ? constexpr foo : bar;

My motivation was that I had a std::variant visitor that was identical for all types except one. So instead of writing an overload set for std::visit, it was simpler to have one common lambda with a conditional inside. Something like this, which returns “int” for int and “other” for all other types in the variant:

std::visit([]<typename T>(T value) {
        if constexpr(std::is_same_v<int, T>)
            return "int";
            return "other";

It would be nicer to write it like this with a conditional operator, but now we can’t use constexpr.

std::visit([]<typename T>(T value) {
        return std::is_same_v<int, T> ? "int" : "other";

So I had the idea of constexpr conditional operator, so I could write my lambda something like this:

std::visit([]<typename T>(T value) {
         return std::is_same_v<int, T> ? constexpr "int" : "other";

In this case, constexpr doesn’t actually make much of a difference. std::is_same_v is a constant expression no matter if you use the constexpr keyword or not, so the compiler optimises it equally well in either case. But at least we verify that the condition is actually a constant expression. If we mess this up, we get a compiler error.

But the most important advantage of constexpr if is that each branch only need to compile if that branch is taken at compile time. So you can do for instance

template<typename T>
int f(T t) {
    if constexpr(std::is_same_v<T, std::string>)
        return t.size();
        return t;

and this will work both for int and std::string, even if the first branch wouldn’t compile for an int and the second wouldn’t compile for std::string. Remove constexpr above, and you’re in trouble.

As it turns out, this is exactly why constexpr conditional operator might not be such a good idea! Thanks to Daniela Engert who pointed this problem out to me.

The problem

if is a statement, it doesn’t have a type. The conditional operator however is an expression, and has a type!

You can’t assign the result of an if statement to something, it doesn’t have a type or result in a value. The conditional operator does however. And the type of the conditional operator is determined by a set of rules which find a common type for the two branches. For instance:

auto result = false ? std::optional<int>{2} : 0;

The two branches have the types std::optional<int> and int, respectively. The compiler now has to figure out what the type of the expression should be, by trying to form implicit conversion sequences from the first to the other, and vice versa. See [expr.cond] for details. Since one can implicitly convert an int to a std::optional<int>, but not vice versa, the type of the full conditional expression (and thus the type of result) is std::optional<int>.

If we introduced something like ? constexpr here, with the same semantics as if constexpr, suddenly one of the branches would be discarded. And we’d have to do that, since the whole point is that the branch not taken usually doesn’t even compile. So in the case above, the first branch would be discarded, and we’d only be left with the literal 0 which has type int. Left with only the int to deduce a type from, the type of the full conditional expression would now be int instead of std::optional<int>. And Daniela’s argument, which I agree with, is that it could be surprising if the type of an expression changed just by introducing or removing constexpr.

In comparison, remember that an if statement doesn’t result in a value, and doesn’t even have a type. If you want to do the same with an if, you first have to define the result variable, and there’s no way to do that upfront without explicitly deciding on its type:

std::optional<int> result;
if constexpr (false)
    result = std::optional<int>{2};
    result = 0;

Notice here that the type of the value we assign to result is still different based on the constexpr condition, but now there’s no surprise, the resulting type is always the same. Both branches have to result in a type implicitly convertible to std::optional<int>, if they’re ever instantiated.

A counter argument?

There is one final point that needs to be mentioned, where the types of two if constexpr branches actually do influence type deduction. This can happen when you have a function with an auto return type, and you return from inside the if constexpr. Here’s a demonstration with a function template, but it can also happen for regular functions:

template<bool b>
auto f()
    if constexpr (b)
        return std::optional<int>{2};
        return 0;

The return type of f<true> is std::optional<int>, and the return type of f<false> is int. Isn’t this the same problem we just used to argue against constexpr conditional operator? It’s similar, but not the same. The big difference is that removing constexpr in this example doesn’t change the deduced type, it rather causes a compilation error. This is due to, which is very strict about all non-discarded return statements having the same type, not just types that can be implicitly converted to a common type:

If a function with a declared return type that contains a placeholder type has multiple non-discarded return statements, the return type is deduced for each such return statement. If the type deduced is not the same in each deduction, the program is ill-formed.


For constexpr conditional operator, adding/removing constexpr could change a deduced type, which could be surprising. For constexpr if, this doesn’t happen.

What do you think? Should we have constexpr conditional operator or not?

Microsoft C++ versions explained

Microsoft has five different version numbers to think about when it comes to C++. Here’s an attempt to explain what they all mean.

  • Visual Studio release year (the “marketing version number”), e.g. Visual Studio 2022
  • Visual Studio actual version number, e.g. Visual Studio 17.0
  • Visual C++ (MSVC) version, e.g. MSVC 14.30
  • Toolset version, e.g. toolset 143
  • Compiler version, e.g. cl.exe 19.30

Visual Studio versions

What most people will see first is the Visual Studio release year. You’ll download Visual Studio 2022, Visual Studio 2019 etc. These however also have a more normal major.minor versioning scheme, and they bump the major version for every release year. So for instance VS 2017 is version 15, VS 2019 is version 16, and VS 2022 is version 17. Note that the year and the major version are not correlated in any way, except that Visual Studio 2010 just happened to also be version 10.

Visual Studio also has minor releases of each major version. Some examples (there are more minor releases per major than shown here):

Visual Studio 201715.0
Visual Studio 201916.0
Visual Studio 202217.0

source: Wikipedia

Visual C++ versions

Microsoft Visual C++, aka MSVC, ships as a part of Visual Studio, but has its own versioning scheme. Importantly, the major number signifies ABI compatibility, so something compiled with MSVC at one major version number can be linked against something compiled with any other MSVC at the same major version. (Some restrictions apply.) The MSVC major version number luckily gets bumped a lot less often than the Visual Studio version itself. As of Visual Studio 2015, they have kept the MSVC major version at 14. The first digit of the minor version seems to be bumped for each major version of Visual Studio itself. The Visual C++ version number is also used for the Visual C++ Redistributable.

Some examples:

VS YearVS versionMSVC version
Visual Studio 201715.014.1
Visual Studio 201916.014.20
Visual Studio 202217.014.30

source: Wikipedia

The linker (link.exe) also uses the Visual C++ version number as its version number, so e.g. for Visual C++ 14.32 I might see link.exe version 14.32.31332.0.

C++ toolset versions

Closely related to the MSVC version number is the C++ toolset version number. I can’t find a good source for it, but from Microsoft’s article it seems that the toolset version is made up of the MSVC major version and the first digit of the MSVC minor version. Some examples:

VS YearVS versionMSVC versionToolset version
Visual Studio 201715.014.1141
Visual Studio 201916.014.20142
Visual Studio 202217.014.30143

Source: Microsoft

Compiler versions

Finally, there’s the compiler version, which is what cl.exe reports. E.g. 19.16.27048. The major.minor version scheme correlates with the _MSC_VER macro which you can check in your source code (godbolt). So e.g. cl.exe version 19.21 has _MSC_VER 1921. (I’ll be nice and count those as one version number.)

VS YearVS versionMSVC versionToolset versionCompiler version
Visual Studio 201715.014.114119.10
Visual Studio 201916.014.2014219.20
Visual Studio 202217.014.3014319.30

The _MSC_VER version number is incremented monotonically at each Visual C++ toolset update, so if you want to only compile some stuff if the compiler is new enough, you can do e.g. #if _MSC_VER >= 1930.

Appendix: Running out of version numbers

Interestingly, the scheme where they bump the first digit of the Visual C++ minor version for each major release of Visual Studio means that they can only have nine minor versions of MSVC per Visual Studio major version! And looking at wikipedia, it seems they actually ran out of toolset versions at the end of Visual Studio 2019 and reused 14.28 and 14.29 for the final four Visual Studio 2019 releases (Visual Studio 16.8 and 16.9 had MSVC 14.28, Visual Studio 16.10 and 16.11 had MSVC 14.29).

The difference between no move constructor and a deleted move constructor

It’s easy to think that deleting the move constructor means removing it. So if you do MyClass(MyClass&&) = delete , you make sure it doesn’t get a move constructor. This is however not technically correct. It might seem like a nitpick, but it actually gives you a less useful mental model of what’s going on.

First: When does this matter? It matters for understanding in which cases you’re allowed to make a copy/move from an rvalue.

Here are some examples of having to copy/move an object of type MyClass:

MyClass obj2(obj1);
MyClass obj3(std::move(obj1));

MyClass obj4 = obj1;
MyClass obj5 = std::move(obj1);

return obj1;
return std::move(obj1);

These are examples of “direct initialization” (the former two) and “copy initialization” (the latter four). Note that there is no concept of “move initialization” in C++. Whether you end up using the copy or the move constructor to initialize the new object is just a detail. For the rest of this post, let’s just look at copy initialization, direct initialization works the same for our purposes. In any case you create a new copy of the object, and the implementation uses either the copy or the move constructor to do so.

Let’s first look at a class NoMove:

struct NoMove
    NoMove(const NoMove&);

This class has a user-declared copy constructor, so it doesn’t automatically get a move constructor. From the C++ standard

If the definition of a class X does not explicitly declare a move constructor, a non-explicit one will be implicitly declared as defaulted if and only if

– X does not have a user-declared copy constructor,
– (…)

So this class doesn’t have a move constructor at all. You didn’t explicitly declare one, and none got implicitly declared for you.

On the other hand, let’s see what happens if we explicitly delete the move constructor:

struct DeletedMove
    DeletedMove(const DeletedMove&);
    DeletedMove(DeletedMove&&) = delete;

This is called ” a deleted definition”. From the C++ standard:

A function definition of the form:
(…) = delete ;
is called a deleted definition. A function with a deleted definition is also called a deleted function.

Importantly, that does not mean that its definition has been deleted/removed and is no longer there. It means that is has a definition, and that this particular kind of definition is called a “deleted definition”. I like to read it as “deleted-definition”.

So our NoMove class has no move constructor at all. Our DeletedMove class has a move constructor with a deleted definition.

Why does this matter?

Let’s first look at a class with both a copy and a move constructor, and how to copy-initialize it.

struct Movable
    Movable(const Movable&);

Movable movable;
Movable movable2 = movable;

When initializing movable2, we need to find a function to do that with. A copy constructor would do nicely. And since we do have a copy constructor, it indeed gets used for this.

What if we turn movable into an rvalue?

Movable movable2 = std::move(movable);

Now a move constructor would be great. And we do have one, and it indeed gets used.

But what if we didn’t have a move constructor? That’s the case with our class NoMove from above.

struct NoMove
    NoMove(const NoMove&);

This one has a copy constructor, so it doesn’t get a move constructor. We can of course still make copies using the copy constructor:

NoMove noMove;
NoMove noMove2 = noMove;

But what happens now?

NoMove noMove;
NoMove noMove2 = std::move(noMove);

Are we now “move initializing” noMove2 and need the move constructor? Actually, we’re not. We’re still copy-initializing it, and need some function to do that task for us. A move constructor would be great, but a copy constructor would also do. It may be less efficient, but of course you’re allowed to make a copy of an rvalue.

So this is fine, the code compiles, and the copy constructor is used to make a copy of the rvalue.

What happened behind the scenes in all the examples above, is overload resolution. Overload resolution looks at all the candidates to do the job, and picks the best one. In the cases where we initialize from an lvalue, the only candidate is the copy constructor. We’re not allowed to move from an lvalue. In the cases where we initialize from an rvalue, both the copy and the move constructors are candidates. But the move constructor is a better match, as we don’t have to convert the rvalue to an lvalue reference. For Movable, the move constructor got selected. For NoMove, there is no move constructor, so the only candidate is the copy constructor, which gets selected.

Now, let’s look at what’s different when instead of having no move constructor, we have a move constructor with a deleted definition:

struct DeletedMove
    DeletedMove(const DeletedMove&);
    DeletedMove(DeletedMove&&) = delete;

We can of course still copy this one as well:

DeletedMove deletedMove2 = deletedMove;

But what happens if we try to copy-initialize from an rvalue?

DeletedMove deletedMove2 = std::move(deletedMove);

Remember, overload resolution tries to find all candidates to do the copy-initialization. And this class does in fact have both a copy and a move constructor, which are both candidates. The move constructor is picked as the best match, since again we avoid the conversion from an rvalue to an lvalue reference. But the move constructor has a deleted definition, and the program does not compile. From the C++ standard:

A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed. [ Note: This includes calling the function implicitly or explicitly (…) If a function is overloaded, it is referenced only if the function is selected by overload resolution.

The function is being called implicitly here, we’re not manually calling the move constructor. And we can see that this applies because overload resolution selected to use the move constructor with the deleted definition.

So the differences between not declaring a move constructor and defining one as deleted are:

  • The first one does not have a move constructor, the second one has a move constructor with a deleted definition.
  • The first one can be copy-initialized from an rvalue, the second can not.

If you enjoyed this post, you can subscribe to my blog, or follow me on Twitter.

No-one knows the type of char + char

Quick quiz! Given the following:

void f(unsigned int);
void f(int);
void f(char);

Which overload gets called by the following?

char x = 1;
char y = 2;
f(x + y);


  1. f(unsigned int)
  2. f(int)
  3. f(char)
  4. No-one knows the type of char + char

If you answered 4), congratulations! And if you answered 2), maybe you tried the code on your own computer? Most people will get f(int) when they try this code, but this is actually not specified by the standard. The only thing we know for sure is that it’s not 3), f(char)!

Let’s have a look at what’s going on:

Before being passed to operator +, the operands (x and y) go through a conversion. [expr.add]§8.7¶1:

The usual arithmetic conversions are performed for operands of arithmetic or enumeration type.

What are “the usual arithmetic conversions”?


Many binary operators that expect operands of arithmetic or enumeration type cause conversions and yield result types in a similar way. The purpose is to yield a common type, which is also the type of the result. This pattern is called the usual arithmetic conversions, which are defined as follows:
– [a bunch of rules for floats, enums etc]
– Otherwise, the integral promotions (7.6) shall be performed on both operands

So both chars go through integral promotions. Those are defined in [conv.prom]§7.6¶1:

A prvalue of an integer type other than bool, char16_t, char32_t, or wchar_t whose integer conversion rank (7.15) is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int.

So a char gets converted to an int if int can fit all possible values of a char. If not, they get converted to unsigned int. But any char should fit in an int, right? As it turns out, that’s not necessarily the case.

First, int could actually be the same size as char. [basic.fundamental]§6.9.1¶2:

There are five standard signed integer types : “signed char”, “short int”, “int”, “long int”, and “long long int”. In this list, each type provides at least as much storage as those preceding it in the list.

Note that it says “at least as much storage”, it doesn’t have to be more. So for instance you could have an sixteen bit system where both char and int are sixteen bits.

Second, char can be either signed or unsigned, it’s up to the implementation: [basic.fundamental]§6.9.1¶1:

It is implementation-defined whether a char object can hold negative values.

int is signed, so if char is also signed, all possible values of char will fit in an int. However, if char is unsigned, and int and char is the same size, char can actually hold larger values than int!

Let’s see an example. If char and int are both sixteen bits, int (which is always signed) can hold [-32768, 32767]. If char is signed, it can also hold [-32768, 32767], and any char fits in an int. However, if char is unsigned, it can hold [0,65535], half of which fall outside the range of int!

In the former case, chars get promoted to ints, but in the latter case, chars get promoted to unsigned ints before being summed.

So in practice, most systems will call f(int), but some might call f(unsigned int), and they would both be confirming to the standard.

If you enjoyed this post, you can subscribe to my blog, or follow me on Twitter.

static_assert in templates

Quiz time! Which of the following programs can you expect to not compile? For bonus points, which are required by the C++ standard to not compile?

Program 1

struct B{};

template <typename T>
struct A {
//Assume sizeof(B) != 4
static_assert(sizeof(T) == 4);

A<B> a;

Program 2

struct B{};

template <typename T>
struct A {
//Assume sizeof(B) != 4
static_assert(sizeof(T) == 4);

A<B>* a;

Program 3

struct B{};

template <typename T>
struct A {
//Assume sizeof(int) != 4
static_assert(sizeof(int) == 4);

In case you’re not familiar with static_assert, it takes a constant boolean expression, and if it evaluates to false, you get a compilation error. The most basic example is just doing static_assert(false). If you have that anywhere in your program, compilation fails. But let’s start at the beginning:

Program 1

struct B{};

template <typename T>
struct A {
//Assume sizeof(B) != 4
static_assert(sizeof(T) == 4);

A<B> a;

Here we have a class template struct A, which takes a type T as its single template parameter. We then assert that the size of the provided template argument is 4.

We then define a variable a of type A<B>. In order to do that, we need the complete definition of A<B>, so that specialization of A gets implicitly instantiated. In that specialization, sizeof(T) becomes sizeof(B), which is not equal to 4, and compliation fails.

Program 2

struct B{};

template <typename T>
struct A {
//Assume sizeof(B) != 4
static_assert(sizeof(T) == 4);

A<B>* a;

This is the exact same problem as in Program 1, except we only define a pointer to A<B>. Does this result in a implicit instantiation? Let’s have a look at [temp.inst] (§17.7.1) ¶1 in the C++17 standard:

Unless a class template specialization has been explicitly instantiated (17.7.2) or explicitly specialized (17.7.3), the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program.

The class template specialization A<B> has not been explicitly instantiated nor explicitly specialized, so the question is then whether it’s implicitly instantiated. We’re only declaring a pointer to it, which doesn’t require a completely-defined object type, so it’s not instantiated. The program compiles just fine.

Program 3

struct B{};

template <typename T>
struct A {
//Assume sizeof(int) != 4
static_assert(sizeof(int) == 4);

In this variation, we’re asserting on the size of int, rather than the size of the template argument. And given the assumption that sizeof(int) != 4, that assertion will always fail. However, we’re never actually instantiating any specialization of A whatsoever. In Program 2, not instatiating the template allowed us to ignore the static_assert. Does the same apply here? In fact, it doesn’t. Let’s have a look at [temp.inst] (§17.6) ¶8 in the C++17 standard:

The program is ill-formed, no diagnostic required, if:


a hypothetical instantiation of a template immediately following its definition would be ill-formed due to a construct that does not depend on a template parameter

The static_assert(sizeof(int) == 4) does not depend on a template parameter, so if we were to instantiate A immediately following its definition, A would always be ill-formed.

So our program is ill-formed, no diagnostic required.

Now what does that mean? ill-formed is the terminology used by the standard for a program that’s not valid C++, where compilation is required to fail with an error message. Ill-formed, no diagnostic required however means our program is not valid C++, but that the compiler isn’t required to let us know. The standard makes no guarantees on the behaviour of the program, i.e. we have undefined behaviour.

So Program 3 has undefined behaviour. In practice however, both Clang, gcc and msvc gives a compilation error in this case.


Program Because … … the standard says In practice
1 We need the class definition Compilation error Compilation error
2 We don’t need the class definition No error No error
3 The assertion doesn’t depend on T Undefined behaviour Compilation error

If you enjoyed this post, you can subscribe to my blog, or follow me on Twitter.


In this Tweet, @willkirkby posts:


that evaluates as 1 in C/C++, but no, JavaScript is the weird language

C++ is indeed weird, or at least it’s very weakly typed. Let’s go through all the details of what’s going on here:



Starting from the right, "" is a string literal, which gets converted to a pointer, which again gets converted to a bool with the value true. This then gets passed to two operator!s, which flip it to false and back to true again. Finally, operator+ converts the bool true to the int 1.

All of this happens behind our backs, so to speak, as C++ is very eager to do conversions we didn’t explicitly ask it to do. This eagerness to do implicit conversions is by the way why you should always mark your single argument constructors and your converting operators explicit.

Detailed explanation:

Now let’s go through this in detail, quoting the C++17 standard. Starting from the right:

"" is a string literal. [lex.string]¶8:

A narrow string literal has type “array of n const char

Then comes operator!. We have an array of n const char, can we use that for operator!? [expr.unary.op]¶9 says:

The operand of the logical negation operator ! is contextually converted to bool (Clause 7); its value is true if the converted operand is false and false otherwise. The type of the result is bool.

So we need to contextually convert our array of n const char to bool. [conv]¶5 says:

Certain language constructs require that an expression be converted to a Boolean value. An expression e appearing in such a context is said to be contextually converted to bool and is well-formed if and only if the declaration bool t(e); is well-formed, for some invented temporary variable t.

So let’s see where bool t(e); takes us when e is an array of n const char.[conv]¶2:

expressions with a given type will be implicitly converted to other types in several contexts:

  • When used as the source expression for an initialization

In our case, the expression is of type “array of n const char“, and we need a bool. We’re going to need a standard conversion sequence. [conv]¶1:

A standard conversion sequence is a sequence of standard conversions in the following order:

  • (1.1) Zero or one conversion from the following set: lvalue-to-rvalue conversion, array-to-pointer conversion, and function-to-pointer conversion.
  • (1.2) Zero or one conversion from the following set: integral promotions, floating-point promotion, integral conversions, floating-point conversions, floating-integral conversions, pointer conversions, pointer to member conversions, and boolean conversions.— […]

So we can first use an array-to-pointer conversion (1.1) to get from “array of n const char” to a pointer. We can then use a boolean conversion (1.2) to get from pointer to bool.

First, the array-to-pointer conversion, [conv.array]¶1:

An lvalue or rvalue of type “array of N T” […] can be converted to a prvalue of type “pointer to T”. The result is a pointer to the first element of the array.

So we now have a pointer to the first element (the terminating \0).

And then the boolean conversion [conv.bool]¶1:

A prvalue of arithmetic, unscoped enumeration, pointer, or pointer to member type can be converted to a prvalue of type bool. A zero value, null pointer value, or null member pointer value is converted to false; any other value is converted to true.

Since our pointer is not a null pointer, its value converts to true.

Negating this twice with two operator!s is trivial, we end up back at true.

Finally, true is passed to operator+. [expr.unary.op]¶7:

The operand of the unary + operator shall have arithmetic, unscoped enumeration, or pointer type and the result is the value of the argument.

bool is not an arithmetic type, so we need to promote the bool true to an arithmetic type before passing it to opreator+. [conv.prom]¶6:

A prvalue of type bool can be converted to a prvalue of type int, with false becoming zero and true becoming one.

So the bool true becomes the int 1, and we’re done.

If you enjoyed this post, you can subscribe to my blog, or follow me on Twitter.

Why you can’t list-initialize containers of non-copyable types

Have you ever wondered why you can’t list-initialize containers of non-copyable types? This is for instance not possible:

    vector<unique_ptr<int>> vu{
        make_unique<int>(1), make_unique<int>(2)};
    //error: call to implicitly-deleted copy constructor of unique_ptr

If you ever wondered, or if you now are, read on!


Since C++11, you’re probably used to intitalizing containers like this:

    vector<int> vi1{1,2,3};
    vector<int> vi2 = {1,2,3};

This of course also works with user defined types. Let’s say you have a class Copyable, then you can for instance do:

    Copyable c1(1);
    Copyable c2(2);
    vector<Copyable> vc1{c1, c2};
    vector<Copyable> vc2 = {c1, c2};

(Copyable is just an arbitrary class which can be copied. It’s reproduced at the end of the post.)

Now what happens if we have a non-copyable class NonCopyable? (NonCopyable is just an arbitrary class which can be moved but not copied, it too is reproduced at the end of the post.)

    NonCopyable n1(1);
    NonCopyable n2(2);
    vector<NonCopyable> vn1{n1, n2}; //error: call to deleted constructor of 'const NonCopyable'
    vector<NonCopyable> vn2 = {n1, n2}; //error: call to deleted constructor of 'const NonCopyable'

Well, n1 and n2 are lvalues, so no wonder it tries to copy them. What if we turn them into rvalues, either with std::move or by creating temporaries?

    vector<NonCopyable> vn3{std::move(n1), std::move(n2)}; //error: call to deleted constructor of 'const NonCopyable'
    vector<NonCopyable> vn3{NonCopyable(4), NonCopyable(5)}; //error: call to deleted constructor of 'const NonCopyable'

So what’s going on here, why is it trying to copy our rvalues? Let’s see what the standard has to say in [dcl.init.list]¶1:

List-initialization is initialization of an object or reference from a braced-init-list.

A braced-init-list is the {element1, element2, ...} syntax we saw above. The standard continues:

Such an initializer is called an initializer list. (…) List-initialization can occur in direct-initialization or copy-initialization contexts.

So list-initialization applies both to the forms vector<Copyable> vc1{c1, c2} and vector<Copyable> vc2 = {c1, c2}, which we saw above. The former is an example of direct-initialization, the latter of copy-initialization. In both cases, {c1, c2} is the braced-init-list.

(Note that the word copy-initialization here is not what causes a copy. Copy-initialization simply refers to the form T t = expression, which doesn’t necessarily invoke the copy constructor.)

Creating the initializer_list

Now what exactly happens with the braced_init_list, and how do its elements end up inside the container we’re initializing?


An object of type std::initializer_list<E> is constructed from an initializer list as if the implementation generated and materialized (7.4) a prvalue of type “array ofN const E“, where N is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and thestd::initializer_list<E> object is constructed to refer to that array.

So the initializer_list can be thought of as just a wrapper for a temporary array we initialize with the elements in the braced-init-list. Sort of like if we’d been doing this:

    const Copyable arr[2] = {c1, c2};    
    vector<Copyable> vc3(initializer_list<Copyable>(arr, arr+2));

Consuming the initializer_list

Now that our initializer_list has been created and passed to the vector constructor, what can that constructor do with it? How does it get the elements out of the initializer_list and into the vector?

[initializer_list.syn] lists the very sparse interface of std::initializer_list:

constexpr const E* begin() const noexcept; // first element
constexpr const E* end() const noexcept; // one past the last element

There’s no access to the elements as rvalue references, only iterators of pointers to const, so we only get lvalues, and we need to copy. Why is there no access as rvalue references?

As we saw in the quote above, “the std::initializer_list<E> object is constructed to refer to that array.” So it only refers to it, and does not own the elements. In particular, this means that if we copy the initializer_list, we do not copy the elements, we only copy a reference to them. In fact, this is spelled out in a note [initializer_list.syn]¶1:

Copying an initializer list does not copy the underlying elements.

So even if we get passed the initializer_list by value, we do not get a copy of the elements themselves, and it would not be safe to move them out, as another copy of the initializer_list could be used again somewhere else. This is why initializer_list offers no rvalue reference access.


In summary: When you do T t{elm1, elm2}, an initializer_list is created, referring to those elements. Copying that initializer_list does not copy the elements. When a constructor takes an initializer_list, it does not know whether it’s the only consumer of those elements, so it’s not safe to move them out of the initializer_list. The only safe way to get the elements out is by copy, so a copy constructor needs to be available.

As usual, the code for this blog post is available on GitHub.

If you enjoyed this post, you can subscribe to my blog, or follow me on Twitter.

Appendix: The Copyable and Uncopyable classes:

class Copyable {
    Copyable(int i): i(i){}
    Copyable(const Copyable&) = default;
    Copyable(Copyable&&) = default;
    Copyable& operator=(const Copyable&) = default;
    Copyable& operator=(Copyable&&) = default;
    ~Copyable() = default;
    int i;

class NonCopyable {
    NonCopyable(int i): i(i){}
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable(NonCopyable&&) = delete;
    NonCopyable& operator=(const NonCopyable&) = default;
    NonCopyable& operator=(NonCopyable&&) = default;
    ~NonCopyable() = default;
    int i;

CppQuiz Android App now available! is an open source C++ quiz site ran by me, with contributions from the C++ community. If you’re unfamiliar with it, you can read more in its “About” section.

A few weeks ago, I was contacted by Sergey Vasilchenko. Without my knowledge, he’d built a prototype Android app for CppQuiz, and asked for my input. Naturally I was delighted, and since then we’ve been collaborating a bit on the project. I’ve been making some adjustments to the backend (basically adding some API functionality to and I’ve also done a bit of testing and review of the app. Most of the work here was by Sergey of course, who implemented the app.

The app works very similarly to the site itself, with the big advantage that it also works when you’re off-line. It downloads all the questions from on startup, and updates its database regularly. Perfect for your daily, bad reception commute!

Currently training mode is the only available mode, but Sergey is working on a quiz mode too. He’s also planning to open source the app in the near future.

So, go download the C++ Quiz App now, and give it a spin!

If you enjoyed this post, you can subscribe to my blog, or follow me on Twitter.