¿Cómo emular el comportamiento de inicialización de la matriz C "int arr [] = {e1, e2, e3, ...}" con std :: array?


137

(Nota: Esta pregunta es no tener que especificar el número de elementos y todavía permitir tipos anidados a ser inicializados directamente.)
Esta pregunta se analizan los usos izquierda para una matriz C como int arr[20];. En su respuesta , @James Kanze muestra una de las últimas fortalezas de las matrices C, sus características de inicialización únicas:

int arr[] = { 1, 3, 3, 7, 0, 4, 2, 0, 3, 1, 4, 1, 5, 9 };

No tenemos que especificar el número de elementos, ¡hurra! Ahora repita sobre él con las funciones de C ++ 11 std::beginy std::enddesde <iterator>( o sus propias variantes ) y nunca tendrá que pensar siquiera en su tamaño.

Ahora, ¿hay alguna forma (posiblemente TMP) de lograr lo mismo std::array? El uso de macros permitió que se vea mejor. :)

??? std_array = { "here", "be", "elements" };

Editar : La versión intermedia, compilada a partir de varias respuestas, se ve así:

#include <array>
#include <utility>

template<class T, class... Tail, class Elem = typename std::decay<T>::type>
std::array<Elem,1+sizeof...(Tail)> make_array(T&& head, Tail&&... values)
{
  return { std::forward<T>(head), std::forward<Tail>(values)... };
}

// in code
auto std_array = make_array(1,2,3,4,5);

Y emplea todo tipo de cosas interesantes de C ++ 11:

  • Plantillas Variadas
  • sizeof...
  • referencias de valor
  • reenvío perfecto
  • std::array, por supuesto
  • inicialización uniforme
  • omitiendo el tipo de retorno con inicialización uniforme
  • inferencia de tipos ( auto)

Y un ejemplo se puede encontrar aquí .

Sin embargo , como @Johannes señala en el comentario sobre la respuesta de @ Xaade, no puede inicializar tipos anidados con dicha función. Ejemplo:

struct A{ int a; int b; };

// C syntax
A arr[] = { {1,2}, {3,4} };
// using std::array
??? std_array = { {1,2}, {3,4} };

Además, el número de inicializadores se limita al número de argumentos de función y plantilla admitidos por la implementación.


Método variado No es la inicialización, más bien la asignación, pero es lo más cerca que puedo llegar. Para obtener la inicialización, debe tener acceso directo a la memoria.
Lee Louviere

Aparentemente, C ++ 0x admite la sintaxis del inicializador. Increíble. Es como llegar a ser más como C #, con soporte de lenguaje para soporte más complicado. ¿Alguien sabe si tenemos soporte de lenguaje formal para interfaces?
Lee Louviere

10
@Downvoter: ¿Razón?
Xeo

1
Disculpas, ¿cuál es el significado de TMPtu pregunta?
kevinarpe

1
@kevinarpe TMP probablemente significa metaprogramación de plantilla .
BeeOnRope

Respuestas:


63

Lo mejor que puedo pensar es:

template<class T, class... Tail>
auto make_array(T head, Tail... tail) -> std::array<T, 1 + sizeof...(Tail)>
{
     std::array<T, 1 + sizeof...(Tail)> a = { head, tail ... };
     return a;
}

auto a = make_array(1, 2, 3);

Sin embargo, esto requiere que el compilador haga NRVO, y luego también omita la copia del valor devuelto (que también es legal pero no obligatorio). En la práctica, esperaría que cualquier compilador de C ++ pueda optimizarlo de manera tal que sea tan rápido como la inicialización directa.


gcc 4.6.0 no permite que el segundo compile, quejándose de reducir la conversión de double a value_type, ¡pero clang ++ 2.9 está bien con ambos!
Cubbi

20
Es con respuestas como esta que entiendo más lo que dijo Bjarne acerca de sentirse "como un nuevo idioma" :) ¡Plantillas variadas, especificador de devolución tardía y deducción de tipos todo en uno!
Matthieu M.

@Matthieu: ahora agregue referencias de valor, reenvío perfecto e inicialización uniforme a partir del código de @ DeadMG y tendrá muchas nuevas funciones establecidas. :>
Xeo

1
@Cubbi: en realidad, g ++ está aquí: las conversiones de estrechamiento no están permitidas en la inicialización agregada en C ++ 0x (pero sí en C ++ 03: ¡un cambio importante que no conocía!). Eliminaré la segunda make_arrayllamada.
Pavel Minaev

@Cubbi, sí, pero esa es una conversión explícita: también permitiría descargas silenciosas y otras cosas similares. Esto todavía se puede hacer usando static_asserty algo de TMP para detectar cuándo Tailno es convertible implícitamente Ty luego usando T(tail)..., pero eso queda como ejercicio para el lector :)
Pavel Minaev

39

Esperaría un simple make_array.

template<typename ret, typename... T> std::array<ret, sizeof...(T)> make_array(T&&... refs) {
    // return std::array<ret, sizeof...(T)>{ { std::forward<T>(refs)... } };
    return { std::forward<T>(refs)... };
}

1
Eliminar el std::array<ret, sizeof...(T)>en la returndeclaración. Eso obliga sin sentido a que exista un constructor de movimiento en el tipo de matriz (en oposición a una construcción desde T&&) en C ++ 14 y C ++ 11.
Yakk - Adam Nevraumont

77
Me encanta cómo la gente de C ++ llama así de simple :-)
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

20

Combinando algunas ideas de publicaciones anteriores, aquí hay una solución que funciona incluso para construcciones anidadas (probadas en GCC4.6):

template <typename T, typename ...Args>
std::array<T, sizeof...(Args) + 1> make_array(T && t, Args &&... args)
{
  static_assert(all_same<T, Args...>::value, "make_array() requires all arguments to be of the same type."); // edited in
  return std::array<T, sizeof...(Args) + 1>{ std::forward<T>(t), std::forward<Args>(args)...};
}

Curiosamente, can no puede hacer que el valor de retorno sea una referencia de valor de r, que no funcionaría para construcciones anidadas. De todos modos, aquí hay una prueba:

auto q = make_array(make_array(make_array(std::string("Cat1"), std::string("Dog1")), make_array(std::string("Mouse1"), std::string("Rat1"))),
                    make_array(make_array(std::string("Cat2"), std::string("Dog2")), make_array(std::string("Mouse2"), std::string("Rat2"))),
                    make_array(make_array(std::string("Cat3"), std::string("Dog3")), make_array(std::string("Mouse3"), std::string("Rat3"))),
                    make_array(make_array(std::string("Cat4"), std::string("Dog4")), make_array(std::string("Mouse4"), std::string("Rat4")))
                    );

std::cout << q << std::endl;
// produces: [[[Cat1, Dog1], [Mouse1, Rat1]], [[Cat2, Dog2], [Mouse2, Rat2]], [[Cat3, Dog3], [Mouse3, Rat3]], [[Cat4, Dog4], [Mouse4, Rat4]]]

(Para la última salida estoy usando mi bonita impresora ).


En realidad, mejoremos la seguridad de tipo de esta construcción. Definitivamente necesitamos que todos los tipos sean iguales. Una forma es agregar una aserción estática, que he editado anteriormente. La otra forma es habilitar solo make_arraycuando los tipos son iguales, así:

template <typename T, typename ...Args>
typename std::enable_if<all_same<T, Args...>::value, std::array<T, sizeof...(Args) + 1>>::type
make_array(T && t, Args &&... args)
{
  return std::array<T, sizeof...(Args) + 1> { std::forward<T>(t), std::forward<Args>(args)...};
}

De cualquier manera, necesitará el all_same<Args...>rasgo de tipo variadic . Aquí está, generalizando a partir de std::is_same<S, T>(nota que la descomposición es importante para permitir la mezcla de T, T&, T const &etc.):

template <typename ...Args> struct all_same { static const bool value = false; };
template <typename S, typename T, typename ...Args> struct all_same<S, T, Args...>
{
  static const bool value = std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value && all_same<T, Args...>::value;
};
template <typename S, typename T> struct all_same<S, T>
{
  static const bool value = std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value;
};
template <typename T> struct all_same<T> { static const bool value = true; };

Tenga en cuenta que las make_array()devoluciones por copia de temporal, que el compilador (¡con suficientes indicadores de optimización!) Puede tratar como un valor r u optimizar de otro modo, y std::arrayes un tipo agregado, por lo que el compilador es libre de elegir el mejor método de construcción posible .

Finalmente, tenga en cuenta que no puede evitar la construcción de copiar / mover cuando make_arrayconfigura el inicializador. Por lo tanto, std::array<Foo,2> x{Foo(1), Foo(2)};no tiene copia / movimiento, pero auto x = make_array(Foo(1), Foo(2));tiene dos copias / movimientos a medida que se envían los argumentos make_array. No creo que pueda mejorar eso, porque no puede pasar una lista de inicializador variadic léxicamente al asistente y deducir el tipo y el tamaño, si el preprocesador tenía una sizeof...función para argumentos variados, tal vez eso podría hacerse, pero no dentro del lenguaje central.


13

El uso de la sintaxis de retorno final make_arraypuede simplificarse aún más

#include <array>
#include <type_traits>
#include <utility>

template <typename... T>
auto make_array(T&&... t)
  -> std::array<std::common_type_t<T...>, sizeof...(t)>
{
  return {std::forward<T>(t)...};
}

int main()
{
  auto arr = make_array(1, 2, 3, 4, 5);
  return 0;
}

Desafortunadamente para las clases agregadas, requiere una especificación de tipo explícita

/*
struct Foo
{
  int a, b;
}; */

auto arr = make_array(Foo{1, 2}, Foo{3, 4}, Foo{5, 6});

De hecho, esta make_arrayimplementación aparece en el operador sizeof ...


versión c ++ 17

Gracias a la deducción de argumentos de plantilla para la propuesta de plantillas de clase , podemos usar guías de deducción para deshacernos del make_arrayayudante

#include <array>

namespace std
{
template <typename... T> array(T... t)
  -> array<std::common_type_t<T...>, sizeof...(t)>;
}

int main()
{
  std::array a{1, 2, 3, 4};
  return 0; 
}

Compilado con -std=c++1zbandera bajo x86-64 gcc 7.0


66
C ++ 17 ya debería tener una guía de deducciones para esto: en.cppreference.com/w/cpp/container/array/deduction_guides
underscore_d

6

Sé que ha pasado bastante tiempo desde que se hizo esta pregunta, pero creo que las respuestas existentes todavía tienen algunas deficiencias, por lo que me gustaría proponer mi versión ligeramente modificada. Los siguientes son los puntos que creo que faltan algunas respuestas existentes.


1. No es necesario confiar en RVO

Algunas respuestas mencionan que necesitamos confiar en RVO para devolver lo construido array. Eso no es verdad; podemos hacer uso de copy-list-initialization para garantizar que nunca se crearán temporarios. Entonces en lugar de:

return std::array<Type, …>{values};

deberiamos:

return {{values}};

2. Hacer make_arrayuna constexprfunción

Esto nos permite crear matrices constantes en tiempo de compilación.

3. No es necesario verificar que todos los argumentos sean del mismo tipo

En primer lugar, si no lo están, el compilador emitirá una advertencia o error de todos modos porque la inicialización de la lista no permite el estrechamiento. En segundo lugar, incluso si realmente decidimos hacer lo nuestro static_assert(quizás para proporcionar un mejor mensaje de error), probablemente deberíamos comparar los tipos deteriorados de los argumentos en lugar de los tipos sin formato. Por ejemplo,

volatile int a = 0;
const int& b = 1;
int&& c = 2;

auto arr = make_array<int>(a, b, c);  // Will this work?

Si simplemente estamos static_asserthaciendo eso a, by ctenemos el mismo tipo, entonces esta verificación fallará, pero eso probablemente no sea lo que esperaríamos. En cambio, debemos comparar sus std::decay_t<T>tipos (que son todos ints).

4. Deduce el tipo de valor de matriz decayendo los argumentos reenviados

Esto es similar al punto 3. Usando el mismo fragmento de código, pero esta vez no especifique explícitamente el tipo de valor:

volatile int a = 0;
const int& b = 1;
int&& c = 2;

auto arr = make_array(a, b, c);  // Will this work?

Probablemente queremos hacer un array<int, 3>, pero las implementaciones en las respuestas existentes probablemente no lo logren. Lo que podemos hacer es, en lugar de devolver a std::array<T, …>, devolver a std::array<std::decay_t<T>, …>.

Hay una desventaja en este enfoque: ya no podemos devolver un arraytipo de valor calificado por cv. Pero la mayoría de las veces, en lugar de algo así como array<const int, …>, usaríamos un de const array<int, …>todos modos. Hay una compensación, pero creo que es razonable. El C ++ 17 std::make_optionaltambién toma este enfoque:

template< class T > 
constexpr std::optional<std::decay_t<T>> make_optional( T&& value );

Teniendo en cuenta los puntos anteriores, una implementación de trabajo completa de make_arrayC ++ 14 se ve así:

#include <array>
#include <type_traits>
#include <utility>

template<typename T, typename... Ts>
constexpr std::array<std::decay_t<T>, 1 + sizeof... (Ts)>
make_array(T&& t, Ts&&... ts)
    noexcept(noexcept(std::is_nothrow_constructible<
                std::array<std::decay_t<T>, 1 + sizeof... (Ts)>, T&&, Ts&&...
             >::value))

{
    return {{std::forward<T>(t), std::forward<Ts>(ts)...}};
}

template<typename T>
constexpr std::array<std::decay<T>_t, 0> make_array() noexcept
{
    return {};
}

Uso:

constexpr auto arr = make_array(make_array(1, 2),
                                make_array(3, 4));
static_assert(arr[1][1] == 4, "!");

6

C ++ 11 admitirá esta forma de inicialización para (¿la mayoría?) Contenedores estándar.


1
Sin embargo, creo que OP no quiere especificar el tamaño de la matriz, pero el tamaño es un parámetro de plantilla de std :: array. Entonces necesita algo como std :: array <unsigned int, 5> n = {1,2,3,4,5};
juanchopanza

std::vector<>no necesita el entero explícito, y no estoy seguro de por qué lo std::arrayharía.
Richard

@ Richard, porque std :: vector tiene un tamaño dinámico y std :: array tiene un tamaño fijo. Vea esto: en.wikipedia.org/wiki/Array_(C%2B%2B)
juanchopanza

@juanchopanza pero la {...}sintaxis implica una extensión constante en tiempo de compilación, por lo que el ctor debería poder deducir la extensión.
Richard

1
std::initializer_list::sizeno es una constexprfunción y, por lo tanto, no se puede usar así. Sin embargo, hay planes de libstdc ++ (la implementación que se envía con GCC) para tener su versión constexpr.
Luc Danton

5

(Solución por @dyp)

Nota: requiere C ++ 14 ( std::index_sequence). Aunque uno podría implementar std::index_sequenceen C ++ 11.

#include <iostream>

// ---

#include <array>
#include <utility>

template <typename T>
using c_array = T[];

template<typename T, size_t N, size_t... Indices>
constexpr auto make_array(T (&&src)[N], std::index_sequence<Indices...>) {
    return std::array<T, N>{{ std::move(src[Indices])... }};
}

template<typename T, size_t N>
constexpr auto make_array(T (&&src)[N]) {
    return make_array(std::move(src), std::make_index_sequence<N>{});
}

// ---

struct Point { int x, y; };

std::ostream& operator<< (std::ostream& os, const Point& p) {
    return os << "(" << p.x << "," << p.y << ")";
}

int main() {
    auto xs = make_array(c_array<Point>{{1,2}, {3,4}, {5,6}, {7,8}});

    for (auto&& x : xs) {
        std::cout << x << std::endl;
    }

    return 0;
}

Pasé por alto la inicialización predeterminada de los elementos std :: array. Actualmente buscando una solución.
Gabriel García

@dyp Actualicé la respuesta con tu código. Si decides escribir tu propia respuesta, avísame y la derribaré. Gracias.
Gabriel Garcia

1
No, esta bien. Vincular una matriz temporal para deducir la longitud es su idea, y no verifiqué si mi código incluso se compila. Creo que sigue siendo su solución, y responda, con un poco de refinamiento;) Sin embargo, uno podría argumentar que no hay beneficio para una variable make_arraycomo en la respuesta de Puppy.
dyp

Correcto. Además, las plantillas no pueden deducir los tipos de las listas de inicializadores, que es uno de los requisitos de la pregunta (inicialización anidada).
Gabriel García

1

Implementación compacta С ++ 17.

template <typename... T>
constexpr auto array_of(T&&... t) {
    return std::array{ static_cast<std::common_type_t<T...>>(t)... };
}


0

Crea un tipo de generador de matriz.

Se sobrecarga operator,para generar una plantilla de expresión que encadena cada elemento al anterior a través de referencias.

Agregue una finishfunción gratuita que tome el generador de matriz y genere una matriz directamente de la cadena de referencias.

La sintaxis debería verse así:

auto arr = finish( make_array<T>->* 1,2,3,4,5 );

No permite la {}construcción basada, como solo lo operator=hace. Si está dispuesto a usarlo =, podemos hacerlo funcionar:

auto arr = finish( make_array<T>= {1}={2}={3}={4}={5} );

o

auto arr = finish( make_array<T>[{1}][{2}[]{3}][{4}][{5}] );

Ninguno de estos parece buenas soluciones.

El uso de variadics lo limita a su límite impuesto por el compilador en el número de varargs y bloquea el uso recursivo de {}subestructuras.

Al final, realmente no hay una buena solución.

Lo que hago es escribir mi código para que consuma ambos T[]y los std::arraydatos agnósticamente , no me importa cuál lo alimente. A veces esto significa que mi código de reenvío tiene que convertir cuidadosamente las []matrices en std::arrays de forma transparente.


1
"Estas no parecen buenas soluciones". Es lo que yo también diría: p
caps
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.