initializer_list e mover semântica


97

Posso mover elementos de um 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?
    }
}

Uma vez que std::intializer_list<T>requer atenção especial do compilador e não tem semântica de valor como os contêineres normais da biblioteca padrão C ++, prefiro prevenir do que remediar e perguntar.


A linguagem central define que os objetos referidos por um initializer_list<T>são não constantes. Like, initializer_list<int>refere-se a intobjetos. Mas eu acho que é um defeito - a intenção é que os compiladores possam alocar estaticamente uma lista na memória somente leitura.
Johannes Schaub - litb

Respostas:


89

Não, isso não funcionará como planejado; você ainda receberá cópias. Estou bastante surpreso com isso, pois pensei que initializer_listexistia para manter uma série de temporários até que eles movemorressem.

begine endpara initializer_listretorno const T *, então o resultado de moveem seu código é T const &&- uma referência rvalue imutável. Essa expressão não pode ser mudada de forma significativa. Ele será vinculado a um parâmetro de função do tipo T const &porque rvalues ​​se vinculam a referências const lvalue, e você ainda verá a semântica de cópia.

Provavelmente a razão para isso é que o compilador pode optar por fazer initializer_listuma constante inicializada estaticamente, mas parece que seria mais limpo fazer seu tipo initializer_listou const initializer_lista critério do compilador, então o usuário não sabe se deve esperar um constou mutável resultado de begine end. Mas isso é apenas meu pressentimento, provavelmente há um bom motivo para estar errado.

Atualização: eu escrevi uma proposta ISO para initializer_listsuporte de tipos apenas de movimento. É apenas um primeiro rascunho e ainda não foi implementado em lugar nenhum, mas você pode ver para mais análises do problema.


11
Caso não esteja claro, ainda significa que o uso std::moveé seguro, se não produtivo. (Excluindo os T const&&construtores de movimento.)
Luc Danton

Eu não acho que você poderia apresentar todo o argumento, const std::initializer_list<T>ou apenas std::initializer_list<T>de uma forma que não cause surpresas com frequência. Considere que cada argumento no initializer_listpode ser um constou outro e que é conhecido no contexto do chamador, mas o compilador deve gerar apenas uma versão do código no contexto do chamado (ou seja, dentro foodele não sabe nada sobre os argumentos que o
autor

1
@David: Bom ponto, mas ainda seria útil que uma std::initializer_list &&sobrecarga fizesse algo, mesmo se uma sobrecarga de não referência também fosse necessária. Suponho que ficaria ainda mais confuso do que a situação atual, que já é ruim.
Potatoswatter

1
@JBJansen Não pode ser hackeado. Não vejo exatamente o que esse código deve realizar wrt initializer_list, mas como usuário, você não tem as permissões necessárias para mover a partir dele. O código seguro não o fará.
Potatoswatter de

1
@Potatoswatter, comentário tardio, mas qual é o status da proposta. Existe alguma chance remota de chegar ao C ++ 20?
WhiZTiM

20
bar(std::move(*it));   // kosher?

Não da maneira que você pretende. Você não pode mover um constobjeto. E std::initializer_listapenas fornece constacesso aos seus elementos. Portanto, o tipo de ité const T *.

Sua tentativa de ligar std::move(*it)resultará apenas em um valor l. IE: uma cópia.

std::initializer_listfaz referência à memória estática . É para isso que serve a aula. Você não pode se mover da memória estática, porque o movimento implica em mudá-la. Você só pode copiar dele.


Um const xvalue ainda é um xvalue e faz initializer_listreferência à pilha, se necessário. (Se o conteúdo não for constante, ainda é seguro para thread.)
Potatoswatter de

5
@Potatoswatter: Você não pode mover de um objeto constante. O initializer_listpróprio objeto pode ser um valor x, mas seu conteúdo (a matriz real de valores para a qual ele aponta) é const, porque esses conteúdos podem ser valores estáticos. Você simplesmente não pode mover-se do conteúdo de um initializer_list.
Nicol Bolas de

Veja minha resposta e sua discussão. Ele moveu o iterador não constreferenciado , produzindo um valor x. movepode não ter sentido, mas é legal e até mesmo possível declarar um parâmetro que aceite exatamente isso. Se mover um tipo específico por acaso for um no-op, pode até funcionar corretamente.
Potatoswatter de

1
@Potatoswatter: O padrão C ++ 11 gasta muita linguagem garantindo que objetos não temporários não sejam realmente movidos a menos que você os use std::move. Isso garante que você possa saber, por inspeção, quando uma operação de movimentação ocorre, uma vez que afeta tanto a origem quanto o destino (você não quer que isso aconteça implicitamente para objetos nomeados). Por causa disso, se você usar std::moveem um lugar onde uma operação de movimentação não acontece (e nenhum movimento real acontecerá se você tiver um valor constx), então o código é enganoso. Acho que é um erro std::movepoder ser chamado em um constobjeto.
Nicol Bolas de

1
Talvez, mas ainda aceitarei menos exceções às regras sobre a possibilidade de código enganoso. De qualquer forma, é exatamente por isso que respondi "não", embora seja legal, e o resultado seja um valor x, mesmo que só seja vinculado como um valor const. Para ser honesto, eu já tive um breve flerte const &&em uma classe de coleta de lixo com ponteiros gerenciados, onde tudo relevante era mutável e mover movia o gerenciamento de ponteiros, mas não afetava o valor contido. Sempre existem casos extremos complicados: v).
Potatoswatter de

2

Isso não funcionará como declarado, porque list.begin()tem tipo const T *e não há como mover de um objeto constante. Os designers da linguagem provavelmente fizeram isso para permitir que as listas de inicializadores contivessem, por exemplo, constantes de string, das quais seria inadequado mover.

No entanto, se você estiver em uma situação em que sabe que a lista de inicializadores contém expressões rvalue (ou deseja forçar o usuário a escrevê-las), existe um truque que fará com que funcione (fui inspirado pela resposta de Sumant para isso, mas a solução é muito mais simples do que essa). Você precisa que os elementos armazenados na lista do inicializador não sejam Tvalores, mas sim valores que encapsulam T&&. Então, mesmo que esses próprios valores sejam constqualificados, eles ainda podem recuperar um rvalue modificável.

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
};

Agora, em vez de declarar um initializer_list<T>argumento, você declara um initializer_list<rref_capture<T> >argumento. Aqui está um exemplo concreto, envolvendo um vetor de std::unique_ptr<int>ponteiros inteligentes, para o qual apenas a semântica de movimento é definida (portanto, esses objetos nunca podem ser armazenados em uma lista de inicializadores); ainda assim, a lista de inicializadores abaixo compila sem 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;
}

Uma pergunta precisa de uma resposta: se os elementos da lista de inicializadores devem ser prvalues ​​verdadeiros (no exemplo, são valores x), a linguagem garante que o tempo de vida dos temporários correspondentes se estenda até o ponto em que são usados? Francamente, não acho que a seção 8.5 relevante da norma trate desse assunto. No entanto, lendo 1.9: 10, parece que a expressão completa relevante em todos os casos engloba o uso da lista de inicializadores, então eu acho que não há perigo de referências rvalue penduradas.


Constantes de string? Gostou "Hello world"? Se você mover a partir deles, apenas copie um ponteiro (ou vincule uma referência).
dyp

1
"Uma pergunta precisa de uma resposta" Os inicializadores internos {..}são vinculados a referências no parâmetro de função de rref_capture. Isso não estende sua vida útil, eles ainda são destruídos no final da expressão completa em que foram criados.
dyp

De acordo com o comentário de outra resposta do TC : Se você tiver múltiplas sobrecargas do construtor, envolva-o std::initializer_list<rref_capture<T>>em algum traço de transformação de sua escolha - digamos, std::decay_t- para bloquear deduções indesejadas.
Reintegrar Monica em

2

Achei que seria instrutivo oferecer um ponto de partida razoável para uma solução alternativa.

Comentários embutidos.

#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));
}

A questão era se um initializer_listpode ser movido, e não se alguém tinha soluções alternativas. Além disso, o principal argumento de venda de initializer_listé que ele é modelado apenas no tipo de elemento, não no número de elementos e, portanto, não exige que os destinatários também sejam modelados - e isso perde completamente isso.
underscore_d

1
@underscore_d você está absolutamente certo. Eu entendo que compartilhar conhecimento relacionado à questão é uma coisa boa por si só. Nesse caso, talvez tenha ajudado o OP e talvez não - ele não respondeu. Na maioria das vezes, porém, o OP e outros recebem bem o material extra relacionado à questão.
Richard Hodges

Claro, pode realmente ajudar os leitores que desejam algo semelhante, initializer_listmas não estão sujeitos a todas as restrições que o tornam útil. :)
underscore_d

@underscore_d qual das restrições eu ignorei?
Richard Hodges

Tudo o que quero dizer é que initializer_list(por meio da mágica do compilador) evita ter de funções de modelo no número de elementos, algo que é inerentemente exigido por alternativas baseadas em matrizes e / ou funções variáveis, restringindo assim a gama de casos em que as últimas são utilizáveis. Pelo meu entendimento, esta é precisamente uma das principais razões para ter initializer_list, então parecia valer a pena mencionar.
sublinhado_d

0

Parece não permitido no padrão atual como já respondido . Aqui está outra solução alternativa para alcançar algo semelhante, definindo a função como variável em vez de obter uma 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;
}

Os modelos de variáveis ​​podem manipular referências de valor r apropriadamente, ao contrário de initializer_list.

Neste código de exemplo, usei um conjunto de pequenas funções auxiliares para converter os argumentos variados em um vetor, para torná-lo semelhante ao código original. Mas é claro que você pode escrever uma função recursiva com modelos variadic diretamente.


A questão era se um initializer_listpode ser movido, e não se alguém tinha soluções alternativas. Além disso, o principal argumento de venda de initializer_listé que ele é modelado apenas no tipo de elemento, não no número de elementos e, portanto, não exige que os destinatários também sejam modelados - e isso perde completamente isso.
underscore_d

0

Eu tenho uma implementação muito mais simples que faz uso de uma classe de invólucro que atua como uma tag para marcar a intenção de mover os elementos. Este é um custo de tempo de compilação.

A classe wrapper é projetada para ser usada da maneira como std::moveé usada, apenas substitua std::movepor move_wrapper, mas isso requer C ++ 17. Para especificações mais antigas, você pode usar um método de construtor adicional.

Você precisará escrever métodos / construtores de construtor que aceitem classes de invólucro dentro initializer_liste mover os elementos de acordo.

Se você precisar que alguns elementos sejam copiados em vez de movidos, construa uma cópia antes de passá-la para initializer_list.

O código deve ser 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)
}

0

Em vez de usar um std::initializer_list<T>, você pode declarar seu argumento como uma referência de array rvalue:

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)});
}

Veja o exemplo usando std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6


-1

Considere o in<T>idioma descrito em cpptruths . A idéia é determinar lvalue / rvalue em tempo de execução e então chamar move ou copy-construction. in<T>irá detectar rvalue / lvalue mesmo que a interface padrão fornecida por initializer_list seja uma referência const.


4
Por que diabos você desejaria determinar a categoria de valor em tempo de execução quando o compilador já sabe disso?
fredoverflow

1
Por favor, leia o blog e deixe-me um comentário se você discordar ou tiver uma alternativa melhor. Mesmo que o compilador conheça a categoria de valor, initializer_list não a preserva porque tem apenas iteradores const. Portanto, você precisa "capturar" a categoria de valor ao construir a initializer_list e passá-la para que a função possa fazer uso dela como desejar.
Sumant de

5
Essa resposta é basicamente inútil sem seguir o link, e as respostas do SO devem ser úteis sem seguir os links.
Yakk - Adam Nevraumont

1
@Sumant [copiando meu comentário de um post idêntico em outro lugar] Essa bagunça colossal realmente fornece algum benefício mensurável para o desempenho ou uso de memória e, se for o caso, uma quantidade suficientemente grande de tais benefícios para compensar adequadamente como parece terrível e o fato de que leva cerca de uma hora para descobrir o que está tentando fazer? Eu meio que duvido.
underscore_d
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.