La lista de inicializadores C ++ 11 falla, pero solo en listas de longitud 2

¡Localicé un oscuro error de registro al hecho de que las listas de inicializadores de longitud 2 parecen ser un caso especial! ¿Cómo es esto posible?

El código fue comstackdo con Apple LLVM versión 5.1 (clang-503.0.40), usando CXXFLAGS=-std=c++11 -stdlib=libc++ .

 #include  #include  #include  using namespace std; typedef vector Strings; void print(string const& s) { printf(s.c_str()); printf("\n"); } void print(Strings const& ss, string const& name) { print("Test " + name); print("Number of strings: " + to_string(ss.size())); for (auto& s: ss) { auto t = "length = " + to_string(s.size()) + ": " + s; print(t); } print("\n"); } void test() { Strings a{{"hello"}}; print(a, "a"); Strings b{{"hello", "there"}}; print(b, "b"); Strings c{{"hello", "there", "kids"}}; print(c, "c"); Strings A{"hello"}; print(A, "A"); Strings B{"hello", "there"}; print(B, "B"); Strings C{"hello", "there", "kids"}; print(C, "C"); } int main() { test(); } 

Salida:

 Test a Number of strings: 1 length = 5: hello Test b Number of strings: 1 length = 8: hello Test c Number of strings: 3 length = 5: hello length = 5: there length = 4: kids Test A Number of strings: 1 length = 5: hello Test B Number of strings: 2 length = 5: hello length = 5: there Test C Number of strings: 3 length = 5: hello length = 5: there length = 4: kids 

También debo agregar que la longitud de la cadena falsa en la prueba b parece ser indeterminada, siempre es mayor que la primera cadena de inicialización, pero ha variado desde una más que la longitud de la primera cadena hasta el total de las longitudes de las dos cadenas en el inicializador.

Introducción

Imagine la siguiente statement y uso:

 struct A { A (std::initializer_list); }; 

 A {{"a" }}; // (A), initialization of 1 string A {{"a", "b" }}; // (B), initialization of 1 string << !! A {{"a", "b", "c"}}; // (C), initialization of 3 strings 

En ( A ) y ( C ), cada cadena c-style está causando la inicialización de una (1) std :: cadena , pero, como ha indicado en su pregunta, ( B ) es diferente.

El comstackdor ve que es posible construir una std :: cadena usando un iterador de inicio y final , y luego de la instrucción de análisis ( B ) preferirá dicha construcción sobre el uso de "a" y "b" como inicializadores individuales para dos elementos.

 A { std::string { "a", "b" } }; // the compiler's interpretation of (B) 

Nota : El tipo de "a" y "b" es char const[2] , un tipo que puede degradarse implícitamente en una char const* , un tipo de puntero que es adecuado para actuar como un iterador que denota comienzo o final al crear a std :: string ... pero debemos tener cuidado: estamos causando un comportamiento indefinido ya que no existe una relación (garantizada) entre los dos punteros al invocar dicho constructor.


Explicación

Cuando invocas un constructor que toma una std :: initializer_list usando dobles llaves {{ a, b, ... }} , hay dos posibles interpretaciones:

  1. Las llaves externas se refieren al propio constructor, las llaves internas denotan los elementos para tomar parte en std :: initializer_list , o:

  2. Las llaves externas se refieren a std :: initializer_list , mientras que las llaves internas denotan la inicialización de un elemento dentro de ella.

Se prefiere hacer 2) siempre que sea posible, y dado que std::string tiene un constructor que toma dos iteradores, es el que se llama cuando se tiene std::vector {{ "hello", "there" }} .

Ejemplo adicional:

 std::vector {{"this", "is"}, {"stackoverflow"}}.size (); // yields 2 

Solución

No use llaves dobles para tal inicialización.

En primer lugar, este es un comportamiento indefinido a menos que me pierda algo obvio. Ahora déjame explicarte. El vector se está construyendo a partir de una lista inicial de cadenas. Sin embargo, esta lista solo contiene una cadena. Esta cadena está formada por el interno {"Hello", "there"} . ¿Cómo? Con el constructor del iterador. Esencialmente, for (auto it = "Hello"; it != "there"; ++it) está formando una cadena que contiene Hello\0 .

Para un simple ejemplo, mira aquí . Si bien UB es una razón suficiente, parecería que el segundo literal se está colocando justo después del primero en la memoria. Como beneficio adicional, haga "Hello", "Hello" y probablemente obtendrá una cadena de longitud 0. Si no entiende nada aquí, le recomiendo leer la excelente respuesta de Filip .