initializer_list y mover semántica

97

¿Puedo sacar elementos de un std::initializer_list<T>?

#include <initializer_list>
#include <utility>

template<typename T>
void foo(std::initializer_list<T> list)
{
    for (auto it = list.begin(); it != list.end(); ++it)
    {
        bar(std::move(*it));   // kosher?
    }
}

Dado que std::intializer_list<T>requiere atención especial del compilador y no tiene una semántica de valor como los contenedores normales de la biblioteca estándar de C ++, prefiero prevenir que lamentar y preguntar.

fredoverflow
fuente
Los define lengua de la base de que el objeto referido por una initializer_list<T>son no -const. Me gusta, se initializer_list<int>refiere a intobjetos. Pero creo que es un defecto: se pretende que los compiladores puedan asignar estáticamente una lista en la memoria de solo lectura.
Johannes Schaub - litb

Respuestas:

89

No, eso no funcionará como se esperaba; todavía recibirá copias. Estoy bastante sorprendido por esto, ya que pensé que initializer_listexistía para mantener una serie de provisionales hasta que fueran moveeliminados.

beginy endpara initializer_listdevolución const T *, por lo que el resultado de moveen su código es T const &&: una referencia rvalue inmutable. Esa expresión no se puede mover de manera significativa. Se vinculará a un parámetro de función de tipo T const &porque los rvalues ​​se vinculan a las referencias const lvalue, y aún verá la semántica de copia.

Probablemente la razón de esto es que el compilador puede optar por hacer initializer_listuna constante inicializada estáticamente, pero parece que sería más limpio hacer su tipo initializer_listo const initializer_lista discreción del compilador, por lo que el usuario no sabe si esperar una consto mutable. resultado de beginy end. Pero ese es solo mi presentimiento, probablemente haya una buena razón por la que estoy equivocado.

Actualización: escribí una propuesta ISO para initializer_listadmitir tipos de solo movimiento. Es solo un primer borrador y aún no está implementado en ninguna parte, pero puede verlo para analizar más el problema.

Potatoswatter
fuente
11
En caso de que no esté claro, significa que el uso std::movees seguro, si no productivo. (Excepto los T const&&constructores de movimiento.)
Luc Danton
Tampoco creo que puedas hacer todo el argumento const std::initializer_list<T>o simplemente std::initializer_list<T>de una manera que no cause sorpresas muy a menudo. Considere que cada argumento en el initializer_listpuede ser uno consto no y que se conoce en el contexto del llamador, pero el compilador debe generar solo una versión del código en el contexto del destinatario (es decir, dentro foono sabe nada sobre los argumentos que está pasando la persona que llama)
David Rodríguez - dribeas
1
@David: Buen punto, pero aún sería útil que una std::initializer_list &&sobrecarga haga algo, incluso si también se requiere una sobrecarga sin referencia. Supongo que sería aún más confuso que la situación actual, que ya es mala.
Potatoswatter
1
@JBJansen No se puede piratear. No veo exactamente lo que se supone que debe lograr ese código wrt initializer_list, pero como usuario no tiene los permisos necesarios para moverse de él. El código seguro no lo hará.
Potatoswatter
1
@Potatoswatter, comentario tardío, pero cuál es el estado de la propuesta. ¿Existe alguna posibilidad remota de que se convierta en C ++ 20?
WhiZTiM
20
bar(std::move(*it));   // kosher?

No de la manera que pretendes. No puedes mover un constobjeto. Y std::initializer_listsolo brinda constacceso a sus elementos. Entonces el tipo de ites const T *.

Su intento de llamar std::move(*it)solo resultará en un valor l. IE: una copia.

std::initializer_listhace referencia a la memoria estática . Para eso es la clase. No puedes moverte de la memoria estática, porque el movimiento implica cambiarla. Solo puede copiar de él.

Nicol Bolas
fuente
Un valor x constante sigue siendo un valor x, y hace initializer_listreferencia a la pila si es necesario. (Si el contenido no es constante, todavía es seguro para subprocesos).
Potatoswatter
5
@Potatoswatter: No puedes moverte de un objeto constante. El initializer_listobjeto en sí puede ser un valor x, pero su contenido (la matriz real de valores a la que apunta) lo es const, porque esos contenidos pueden ser valores estáticos. Simplemente no puede moverse del contenido de un initializer_list.
Nicol Bolas
Vea mi respuesta y su discusión. Ha movido el iterador desreferenciado, produciendo un valor constx. movepuede no tener sentido, pero es legal e incluso posible declarar un parámetro que acepte precisamente eso. Si mover un tipo en particular resulta ser una operación no operativa, incluso podría funcionar correctamente.
Potatoswatter
1
@Potatoswatter: El estándar C ++ 11 gasta mucho lenguaje para garantizar que los objetos no temporales no se muevan realmente a menos que use std::move. Esto asegura que puede saber por inspección cuándo ocurre una operación de movimiento, ya que afecta tanto al origen como al destino (no desea que suceda implícitamente para los objetos con nombre). Por eso, si usa std::moveen un lugar donde no ocurre una operación de movimiento (y no ocurrirá ningún movimiento real si tiene un valor constx), entonces el código es engañoso. Creo que es un error std::moveser invocable en un constobjeto.
Nicol Bolas
1
Quizás, pero aún aceptaré menos excepciones a las reglas sobre la posibilidad de código engañoso. De todos modos, esa es exactamente la razón por la que respondí "no" a pesar de que es legal, y el resultado es un valor x incluso si solo se vincula como un valor constante. Para ser honesto, ya tuve un breve coqueteo con const &&una clase de recolección de basura con punteros administrados, donde todo lo relevante era mutable y el movimiento movió la administración de punteros pero no afectó el valor contenido. Siempre hay casos extremos complicados: v).
Potatoswatter
2

Esto no funcionará como se indica, porque list.begin()tiene tipo const T *y no hay forma de que pueda moverse desde un objeto constante. Los diseñadores del lenguaje probablemente lo hicieron así para permitir que las listas de inicializadores contengan, por ejemplo, constantes de cadena, de las que no sería apropiado moverse.

Sin embargo, si se encuentra en una situación en la que sabe que la lista de inicializadores contiene expresiones rvalue (o si desea obligar al usuario a escribirlas), existe un truco que lo hará funcionar (me inspiré en la respuesta de Sumant para esto, pero la solución es mucho más simple que esa). Necesita que los elementos almacenados en la lista de inicializadores no sean Tvalores, sino valores que encapsulan T&&. Entonces, incluso si esos valores en sí mismos están constcalificados, aún pueden recuperar un rvalue modificable.

template<typename T>
  class rref_capture
{
  T* ptr;
public:
  rref_capture(T&& x) : ptr(&x) {}
  operator T&& () const { return std::move(*ptr); } // restitute rvalue ref
};

Ahora, en lugar de declarar un initializer_list<T>argumento, declara un initializer_list<rref_capture<T> >argumento. Aquí hay un ejemplo concreto, que involucra un vector de std::unique_ptr<int>punteros inteligentes, para el cual solo se define la semántica de movimiento (por lo que estos objetos en sí mismos nunca pueden almacenarse en una lista de inicializadores); sin embargo, la lista de inicializadores a continuación se compila sin problemas.

#include <memory>
#include <initializer_list>
class uptr_vec
{
  typedef std::unique_ptr<int> uptr; // move only type
  std::vector<uptr> data;
public:
  uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {}
  uptr_vec(std::initializer_list<rref_capture<uptr> > l)
    : data(l.begin(),l.end())
  {}
  uptr_vec& operator=(const uptr_vec&) = delete;
  int operator[] (size_t index) const { return *data[index]; }
};

int main()
{
  std::unique_ptr<int> a(new int(3)), b(new int(1)),c(new int(4));
  uptr_vec v { std::move(a), std::move(b), std::move(c) };
  std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl;
}

Una pregunta necesita una respuesta: si los elementos de la lista de inicializadores deben ser valores verdaderos (en el ejemplo son valores x), ¿el lenguaje asegura que la vida útil de los temporales correspondientes se extienda hasta el punto en el que se utilizan? Francamente, no creo que la sección 8.5 relevante de la norma aborde este problema en absoluto. Sin embargo, al leer 1.9: 10, parecería que la expresión completa relevante en todos los casos abarca el uso de la lista de inicializadores, por lo que creo que no hay peligro de que cuelguen referencias de rvalue.

Marc van Leeuwen
fuente
¿Constantes de cadena? Al igual que "Hello world"? Si se aleja de ellos, simplemente copie un puntero (o enlace una referencia).
dyp
1
"Una pregunta necesita una respuesta" Los inicializadores internos {..}están vinculados a referencias en el parámetro de función de rref_capture. Esto no extiende su vida, todavía están destruidos al final de la expresión completa en la que fueron creados.
dyp
Por TC comentario 's de otra respuesta: Si tiene varias sobrecargas del constructor, envolver el std::initializer_list<rref_capture<T>>de algún rasgo de la transformación de su elección - por ejemplo, std::decay_t- para bloquear la deducción no deseado.
Reincorporación a Monica
2

Pensé que sería instructivo ofrecer un punto de partida razonable para una solución.

Comentarios en línea.

#include <memory>
#include <vector>
#include <array>
#include <type_traits>
#include <algorithm>
#include <iterator>

template<class Array> struct maker;

// a maker which makes a std::vector
template<class T, class A>
struct maker<std::vector<T, A>>
{
  using result_type = std::vector<T, A>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const -> result_type
  {
    result_type result;
    result.reserve(sizeof...(Ts));
    using expand = int[];
    void(expand {
      0,
      (result.push_back(std::forward<Ts>(ts)),0)...
    });

    return result;
  }
};

// a maker which makes std::array
template<class T, std::size_t N>
struct maker<std::array<T, N>>
{
  using result_type = std::array<T, N>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const
  {
    return result_type { std::forward<Ts>(ts)... };
  }

};

//
// delegation function which selects the correct maker
//
template<class Array, class...Ts>
auto make(Ts&&...ts)
{
  auto m = maker<Array>();
  return m(std::forward<Ts>(ts)...);
}

// vectors and arrays of non-copyable types
using vt = std::vector<std::unique_ptr<int>>;
using at = std::array<std::unique_ptr<int>,2>;


int main(){
    // build an array, using make<> for consistency
    auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20));

    // build a vector, using make<> because an initializer_list requires a copyable type  
    auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20));
}
Richard Hodges
fuente
La pregunta era si initializer_listse podía mover de una, no si alguien tenía soluciones. Además, el principal punto de venta de initializer_listes que solo se basa en el tipo de elemento, no en la cantidad de elementos, y por lo tanto no requiere que los destinatarios también tengan una plantilla, y esto lo pierde por completo.
underscore_d
1
@underscore_d tienes toda la razón. Considero que compartir el conocimiento relacionado con la pregunta es algo bueno en sí mismo. En este caso, tal vez ayudó al OP y tal vez no, no respondió. Sin embargo, la mayoría de las veces, el OP y otros agradecen material adicional relacionado con la pregunta.
Richard Hodges
Claro, de hecho puede ayudar a los lectores que quieran algo parecido, initializer_listpero que no estén sujetos a todas las restricciones que lo hacen útil. :)
underscore_d
@underscore_d ¿cuál de las restricciones he pasado por alto?
Richard Hodges
Todo lo que quiero decir es que initializer_list(a través de la magia del compilador) evita tener que modelar funciones en el número de elementos, algo que es inherentemente requerido por las alternativas basadas en matrices y / o funciones variadas, limitando así el rango de casos donde estos últimos son utilizables. Según tengo entendido, esta es precisamente una de las principales razones para tener initializer_list, por lo que me pareció digno de mención.
underscore_d
0

Parece que no está permitido en el estándar actual como ya se respondió . Aquí hay otra solución para lograr algo similar, definiendo la función como variable en lugar de tomar una lista de inicializadores.

#include <vector>
#include <utility>

// begin helper functions

template <typename T>
void add_to_vector(std::vector<T>* vec) {}

template <typename T, typename... Args>
void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) {
  vec->push_back(std::forward<T>(car));
  add_to_vector(vec, std::forward<Args>(cdr)...);
}

template <typename T, typename... Args>
std::vector<T> make_vector(Args&&... args) {
  std::vector<T> result;
  add_to_vector(&result, std::forward<Args>(args)...);
  return result;
}

// end helper functions

struct S {
  S(int) {}
  S(S&&) {}
};

void bar(S&& s) {}

template <typename T, typename... Args>
void foo(Args&&... args) {
  std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...);
  for (auto& arg : args_vec) {
    bar(std::move(arg));
  }
}

int main() {
  foo<S>(S(1), S(2), S(3));
  return 0;
}

Las plantillas variables pueden manejar referencias de valor r de manera apropiada, a diferencia de initializer_list.

En este código de ejemplo, utilicé un conjunto de pequeñas funciones auxiliares para convertir los argumentos variadic en un vector, para hacerlo similar al código original. Pero, por supuesto, puede escribir una función recursiva con plantillas variadas directamente en su lugar.

Hiroshi Ichikawa
fuente
La pregunta era si initializer_listse podía mover de una, no si alguien tenía soluciones. Además, el principal punto de venta de initializer_listes que solo se basa en el tipo de elemento, no en la cantidad de elementos, y por lo tanto no requiere que los destinatarios también tengan una plantilla, y esto lo pierde por completo.
underscore_d
0

Tengo una implementación mucho más simple que hace uso de una clase contenedora que actúa como una etiqueta para marcar la intención de mover los elementos. Este es un costo en tiempo de compilación.

La clase contenedora está diseñada para usarse en la forma en que std::movese usa, simplemente reemplácela std::movecon move_wrapper, pero esto requiere C ++ 17. Para especificaciones más antiguas, puede utilizar un método de construcción adicional.

Deberá escribir métodos de constructor / constructores que acepten clases contenedoras dentro initializer_listy mover los elementos en consecuencia.

Si necesita copiar algunos elementos en lugar de moverlos, cree una copia antes de pasarla a initializer_list.

El código debe estar autodocumentado.

#include <iostream>
#include <vector>
#include <initializer_list>

using namespace std;

template <typename T>
struct move_wrapper {
    T && t;

    move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues
    }

    explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move
    }
};

struct Foo {
    int x;

    Foo(int x) : x(x) {
        cout << "Foo(" << x << ")\n";
    }

    Foo(Foo const & other) : x(other.x) {
        cout << "copy Foo(" << x << ")\n";
    }

    Foo(Foo && other) : x(other.x) {
        cout << "move Foo(" << x << ")\n";
    }
};

template <typename T>
struct Vec {
    vector<T> v;

    Vec(initializer_list<T> il) : v(il) {
    }

    Vec(initializer_list<move_wrapper<T>> il) {
        v.reserve(il.size());
        for (move_wrapper<T> const & w : il) {
            v.emplace_back(move(w.t));
        }
    }
};

int main() {
    Foo x{1}; // Foo(1)
    Foo y{2}; // Foo(2)

    Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied
    // Foo(3)
    // copy Foo(2)
    // move Foo(3)
    // move Foo(1)
    // move Foo(2)
}
bumfo
fuente
0

En lugar de usar a std::initializer_list<T>, puede declarar su argumento como una referencia de rvalue de matriz:

template <typename T>
void bar(T &&value);

template <typename T, size_t N>
void foo(T (&&list)[N] ) {
   std::for_each(std::make_move_iterator(std::begin(list)),
                 std::make_move_iterator(std::end(list)),
                 &bar);
}

void baz() {
   foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}

Ver ejemplo usando std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6

Jorge Bellon
fuente
-1

Considere el in<T>idioma descrito en cpptruths . La idea es determinar lvalue / rvalue en tiempo de ejecución y luego llamar a move o copy-construction. in<T>detectará rvalue / lvalue incluso si la interfaz estándar proporcionada por initializer_list es una referencia constante.

Sumant
fuente
4
¿Por qué diablos querría determinar la categoría de valor en tiempo de ejecución cuando el compilador ya la conoce?
fredoverflow
1
Por favor lea el blog y déjeme un comentario si no está de acuerdo o tiene una alternativa mejor. Incluso si el compilador conoce la categoría de valor, initializer_list no la conserva porque solo tiene iteradores constantes. Por lo tanto, debe "capturar" la categoría de valor cuando construye initializer_list y pasarla para que la función pueda utilizarla como le plazca.
Sumant
5
Esta respuesta es básicamente inútil sin seguir el enlace, y las respuestas SO deberían ser útiles sin seguir los enlaces.
Yakk - Adam Nevraumont
1
@Sumant [copiando mi comentario de una publicación idéntica en otro lugar] ¿Ese desorden enorme en realidad proporciona algún beneficio medible para el rendimiento o el uso de la memoria y, de ser así, una cantidad suficientemente grande de tales beneficios para compensar adecuadamente lo terrible que se ve y el hecho de que ¿Tarda aproximadamente una hora en averiguar qué está intentando hacer? Lo dudo un poco.
underscore_d