Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird behaviour constexpr with std::initializer_list

I am trying to understand why the compiler is complaining here:

// cexpr_test.cpp
#include <initializer_list>

constexpr int test_cexpr(std::initializer_list<const char*> x)
{
    return (int) (*x.begin())[0]; // ensuring the value isn't optimized out.
}

int main()
{
    constexpr int r1 = test_cexpr({ "why does this work," });

    constexpr std::initializer_list<const char*> broken { "but this doesn't?" };
    constexpr int r2 = test_cexpr(broken);

    return r1 + r2;
}

The message produced when compiled with

g++ -std=c++11 -Wall -Werror cexpr_test.cpp 

is as follows:

cexpr_test.cpp: In function ‘int main()’: cexpr_test.cpp:12:76: error: ‘const std::initializer_list{((const char* const*)(&)), 1}’ is not a constant expression 12 | constexpr std::initializer_list broken { "but this doesn't?" }; |

It's confusing why it constructs the first initializer list without any issues. What am I missing here?

like image 689
Alec C Avatar asked Dec 31 '19 04:12

Alec C


1 Answers

The problem is with the initialization of broken itself here. What is a std::initializer_list and what does it hold? It's a reference type (i.e. refers somehow to another objects), and it's backed by a c-style array. The properties of this c-style array are what determines if the initializer_list can be a constexpr variable. We can consult [dcl.init.list] for those properties.

5 An object of type std​::​initializer_­list<E> is constructed from an initializer list as if the implementation generated and materialized a prvalue of type “array of N 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 the std​::​initializer_­list<E> object is constructed to refer to that array. [ Note: A constructor or conversion function selected for the copy shall be accessible in the context of the initializer list.  — end note ] If a narrowing conversion is required to initialize any of the elements, the program is ill-formed. [ Example:

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

The initialization will be implemented in a way roughly equivalent to this:

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

assuming that the implementation can construct an initializer_­list object with a pair of pointers.  — end example ]

6 The array has the same lifetime as any other temporary object, except that initializing an initializer_­list object from the array extends the lifetime of the array exactly like binding a reference to a temporary. [ Example:

typedef std::complex<double> cmplx;
std::vector<cmplx> v1 = { 1, 2, 3 };

void f() {
  std::vector<cmplx> v2{ 1, 2, 3 };
  std::initializer_list<int> i3 = { 1, 2, 3 };
}

struct A {
  std::initializer_list<int> i4;
  A() : i4{ 1, 2, 3 } {}  // ill-formed, would create a dangling reference
};

For v1 and v2, the initializer_­list object is a parameter in a function call, so the array created for { 1, 2, 3 } has full-expression lifetime. For i3, the initializer_­list object is a variable, so the array persists for the lifetime of the variable. For i4, the initializer_­list object is initialized in the constructor's ctor-initializer as if by binding a temporary array to a reference member, so the program is ill-formed ([class.base.init]).  — end example ] [ Note: The implementation is free to allocate the array in read-only memory if an explicit array with the same initializer could be so allocated.  — end note ]

So this array is like any other temporary object that is referred to by a constant reference. Which means we can actually reduce your minimal example to something even smaller

constexpr int test_cexpr(int const & x)
{
    return x; 
}

int main()
{
    constexpr int r1 = test_cexpr(0);

    constexpr int const &broken = 0;
    constexpr int r2 = test_cexpr(broken);

    return r1 + r2;
}

This produces the exact same behavior and error. We can pass 0 directly as an argument to the constexpr function, the reference binds, and we can even refer to it inside the function. However, a constexpr reference may not be initialized with 0. The reason for zero not being a valid initializer, is that it requires materializing a temporary int object. This temporary is not a static variable, and so may not be used to initialize a constexpr reference. Simple as that.

The same reasoning applies to your case. The temporary array that's materialized is not an object with static storage duration, so it may not be used to initialize a constexpr reference type.

The reason it works when directly passing the argument to test_cexpr, is that the corresponding parameter is not itself a constexpr variable. Which means it can bind successfully. After which, the thing it's bound to just has to be usable in a constant expression. Without going into too much detail over this: since the temporary in that case has full-expression lifetime (and not lifetime extended), it is usable in a constant expression.

like image 94
StoryTeller - Unslander Monica Avatar answered Oct 04 '22 03:10

StoryTeller - Unslander Monica