C ++ 11 emplace_back no vetor <struct>?

87

Considere o seguinte programa:

#include <string>
#include <vector>

using namespace std;

struct T
{
    int a;
    double b;
    string c;
};

vector<T> V;

int main()
{
    V.emplace_back(42, 3.14, "foo");
}

Não funciona:

$ g++ -std=gnu++11 ./test.cpp
In file included from /usr/include/c++/4.7/x86_64-linux-gnu/bits/c++allocator.h:34:0,
                 from /usr/include/c++/4.7/bits/allocator.h:48,
                 from /usr/include/c++/4.7/string:43,
                 from ./test.cpp:1:
/usr/include/c++/4.7/ext/new_allocator.h: In instantiation of ‘void __gnu_cxx::new_allocator<_Tp>::construct(_Up*, _Args&& ...) [with _Up = T; _Args = {int, double, const char (&)[4]}; _Tp = T]’:
/usr/include/c++/4.7/bits/alloc_traits.h:253:4:   required from ‘static typename std::enable_if<std::allocator_traits<_Alloc>::__construct_helper<_Tp, _Args>::value, void>::type std::allocator_traits<_Alloc>::_S_construct(_Alloc&, _Tp*, _Args&& ...) [with _Tp = T; _Args = {int, double, const char (&)[4]}; _Alloc = std::allocator<T>; typename std::enable_if<std::allocator_traits<_Alloc>::__construct_helper<_Tp, _Args>::value, void>::type = void]’
/usr/include/c++/4.7/bits/alloc_traits.h:390:4:   required from ‘static void std::allocator_traits<_Alloc>::construct(_Alloc&, _Tp*, _Args&& ...) [with _Tp = T; _Args = {int, double, const char (&)[4]}; _Alloc = std::allocator<T>]’
/usr/include/c++/4.7/bits/vector.tcc:97:6:   required from ‘void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {int, double, const char (&)[4]}; _Tp = T; _Alloc = std::allocator<T>]’
./test.cpp:17:32:   required from here
/usr/include/c++/4.7/ext/new_allocator.h:110:4: error: no matching function for call to ‘T::T(int, double, const char [4])’
/usr/include/c++/4.7/ext/new_allocator.h:110:4: note: candidates are:
./test.cpp:6:8: note: T::T()
./test.cpp:6:8: note:   candidate expects 0 arguments, 3 provided
./test.cpp:6:8: note: T::T(const T&)
./test.cpp:6:8: note:   candidate expects 1 argument, 3 provided
./test.cpp:6:8: note: T::T(T&&)
./test.cpp:6:8: note:   candidate expects 1 argument, 3 provided

Qual é a maneira correta de fazer isso e por quê?

(Também tentei chaves simples e duplas)

Andrew Tomazos
fonte
4
Isso funcionará se você fornecer um construtor apropriado.
chris
2
Existe uma maneira de construí-lo no local com o construtor de estrutura de chave criado automaticamente usado por T t{42,3.14, "foo"}?
Andrew Tomazos
4
Não acho que isso tome a forma de um construtor. É inicialização agregada.
chris
5
Não estou tentando afetar a sua opinião de forma alguma .. Mas caso você não tenha prestado atenção a essa questão há muito tempo .. A resposta aceita, com total respeito ao seu redator, não é uma resposta de forma alguma à sua pergunta e pode enganar os leitores.
Humam Helfawi

Respostas:

18

Para qualquer pessoa do futuro, esse comportamento será alterado no C ++ 20 .

Em outras palavras, embora a implementação internamente ainda chame T(arg0, arg1, ...), será considerada regular como T{arg0, arg1, ...}você esperaria.

Red XIII
fonte
93

Você precisa definir explicitamente um ctor para a classe:

#include <string>
#include <vector>

using namespace std;

struct T
{
    int a;
    double b;
    string c;

    T(int a, double b, string &&c) 
        : a(a)
        , b(b)
        , c(std::move(c)) 
    {}
};

vector<T> V;

int main()
{
    V.emplace_back(42, 3.14, "foo");
}

O objetivo do uso emplace_backé evitar a criação de um objeto temporário, que é então copiado (ou movido) para o destino. Embora também seja possível criar um objeto temporário e, em seguida, passá-lo para emplace_back, ele anula (pelo menos a maior parte) o propósito. O que você quer fazer é passar argumentos individuais, então vamos emplace_backchamar o ctor com esses argumentos para criar o objeto no lugar.

Jerry Coffin
fonte
12
Acho que a melhor maneira seria escreverT(int a, double b, string c) : a(a), b(b), c(std::move(c))
balki
9
A resposta aceita anula o propósito de emplace_back. Essa é a resposta correta. É assim que emplace*funciona. Eles constroem o elemento no local usando os argumentos encaminhados. Portanto, um construtor é necessário para receber esses argumentos.
sublinhado_d
1
ainda assim, o vetor poderia fornecer um emplace_aggr, certo?
tamas.kenez
@balki direito, não há nenhum ponto de tomar cpor &&se nada for feito com a sua possível rvalueness; no inicializador do membro, o argumento é tratado como um lvalue novamente, na ausência de um elenco, de modo que o membro apenas obtém uma cópia construída. Mesmo se o membro foi construído por movimento, não é idiomático exigir que os chamadores sempre passem um valor temporário ou std::move()d (embora eu deva confessar que tenho alguns casos em meu código onde faço isso, mas apenas em detalhes de implementação) .
underscore_d
25

Claro, isso não é uma resposta, mas mostra uma característica interessante das tuplas:

#include <string>
#include <tuple>
#include <vector>

using namespace std;

using T = tuple <
    int,
    double,
    string
>;

vector<T> V;

int main()
{
    V.emplace_back(42, 3.14, "foo");
}
rici
fonte
8
Certamente não é uma resposta. o que há de tão interessante nisso? qualquer tipo com um ctor pode ser colocado dessa maneira. tupla tem um ctor. a estrutura do op não. essa é a resposta.
sublinhado_d
6
@underscore_d: Não tenho certeza se me lembro de todos os detalhes do que estava pensando três anos e meio atrás, mas acho que o que estava sugerindo era que se você apenas usar um em tuplevez de definir uma estrutura POD, você obterá um construtor gratuitamente , o que significa que você obtém a emplacesintaxe de graça (entre outras coisas - você também obtém ordenação lexicográfica). Você perde nomes de membros, mas às vezes é menos trabalhoso criar acessadores do que todo o resto do clichê que você precisaria de outra forma. Concordo que a resposta de Jerry Coffin é muito melhor do que a aceita. Eu também votei a favor anos atrás.
rici
3
Sim, soletrar me ajuda a entender o que você quis dizer! Bom ponto. Concordo que às vezes a generalização é suportável quando comparada com outras coisas que o STL fornece para nós: eu uso isso pairquase sempre com ... mas às vezes me pergunto se realmente ganho muito em termos líquidos, heh. Mas talvez tupleisso aconteça no futuro. Obrigado por expandir!
underscore_d
12

Se você não deseja (ou não pode) adicionar um construtor, especialize o alocador para T (ou crie seu próprio alocador).

namespace std {
    template<>
    struct allocator<T> {
        typedef T value_type;
        value_type* allocate(size_t n) { return static_cast<value_type*>(::operator new(sizeof(value_type) * n)); }
        void deallocate(value_type* p, size_t n) { return ::operator delete(static_cast<void*>(p)); }
        template<class U, class... Args>
        void construct(U* p, Args&&... args) { ::new(static_cast<void*>(p)) U{ std::forward<Args>(args)... }; }
    };
}

Observação: a construção da função de membro mostrada acima não pode ser compilada com o clang 3.1 (Desculpe, não sei por quê). Tente o próximo se for usar o clang 3.1 (ou outros motivos).

void construct(T* p, int a, double b, const string& c) { ::new(static_cast<void*>(p)) T{ a, b, c }; }
Mitsuru Kariya
fonte
Em sua função de alocação, você não precisa se preocupar com o alinhamento? Verstd::aligned_storage
Andrew Tomazos
Sem problemas. De acordo com a especificação, os efeitos de "void * :: operator new (size_t size)" são "A função de alocação chamada por uma nova expressão para alocar bytes de tamanho de armazenamento alinhados adequadamente para representar qualquer objeto daquele tamanho."
Mitsuru Kariya
6

Isso parece estar coberto em 23.2.1 / 13.

Primeiro, definições:

Dado um tipo de contêiner X tendo um alocador_type idêntico a A e um valor_type idêntico a T e dado um lvalue m do tipo A, um ponteiro p do tipo T *, uma expressão v do tipo T e um rvalue rv do tipo T, o os seguintes termos são definidos.

Agora, o que o torna construtível no local:

T é EmplaceConstructible em X a partir de args, para zero ou mais argumentos args, significa que a seguinte expressão está bem formada: allocator_traits :: construct (m, p, args);

E, finalmente, uma observação sobre a implementação padrão da chamada de construção:

Nota: Um contêiner chama alocator_traits :: construct (m, p, args) para construir um elemento em p usando args. A construção padrão em std :: allocator chamará :: new ((void *) p) T (args), mas os alocadores especializados podem escolher uma definição diferente.

Isso nos diz que, para um esquema de alocador padrão (e potencialmente o único), você deve ter definido um construtor com o número adequado de argumentos para o que está tentando construir-empor em um contêiner.

Mark B
fonte
-2

você tem que definir um construtor para seu tipo Tporque ele contém um std::stringque não é trivial.

além disso, seria melhor definir (possível inadimplente) mover ctor / atribuir (porque você tem um std::stringmembro móvel ) - isso ajudaria a mover seu T...

ou use apenas T{...}para chamar sobrecarregado emplace_back()conforme recomendado na resposta do neighboug ... tudo depende de seus casos de uso típicos ...

Zaufi
fonte
Um construtor de movimento é gerado automaticamente para T
Andrew Tomazos
1
@ AndrewTomazos-Fathomling: somente se nenhum usuário estiver definido
zaufi
1
Correto, e eles não são.
Andrew Tomazos
@ AndrewTomazos-Fathomling: mas você tem que definir alguns, para evitar instância temporária na emplace_back()chamada :)
zaufi
1
Na verdade, incorreto. Um construtor de movimentação é gerado automaticamente, desde que nenhum destruidor, construtor de cópia ou operadores de atribuição sejam definidos. Definir um construtor de 3 argumentos para uso com emplace_back não suprimirá o construtor de movimento padrão.
Andrew Tomazos
-2

Você pode criar a struct Tinstância e movê-la para o vetor:

V.push_back(std::move(T {42, 3.14, "foo"}));
AlexB
fonte
2
Você não precisa usar std :: move () um objeto temporário T {...}. Já é um objeto temporário (rvalue). Portanto, você pode simplesmente eliminar o std :: move () do seu exemplo.
Nadav Har'El
Além disso, mesmo o nome do tipo T não é necessário - o compilador pode adivinhá-lo. Portanto, apenas um "V.push_back {42, 3.14," foo "}" funcionará.
Nadav Har'El
-8

Você pode usar o {} sintaxe para inicializar o novo elemento:

V.emplace_back(T{42, 3.14, "foo"});

Isso pode ou não ser otimizado, mas deveria ser.

Você tem que definir um construtor para que isso funcione, observe que com seu código você não pode nem fazer:

T a(42, 3.14, "foo");

Mas é disso que você precisa para que o emplace funcione.

então apenas:

struct T { 
  ...
  T(int a_, double b_, string c_) a(a_), b(b_), c(c_) {}
}

fará com que funcione da maneira desejada.

perreal
fonte
10
Isso construirá um temporário e, em seguida, moverá a construção para o array? - ou vai construir o item no local?
Andrew Tomazos
3
Não std::moveé necessário. T{42, 3.14, "foo"}já será encaminhado por emplace_back e vinculado ao construtor struct move como um rvalue. No entanto, eu preferiria uma solução que o construísse no local.
Andrew Tomazos
37
neste caso, mover é quase exatamente equivalente a copiar, portanto, todo o ponto de colocação é perdido.
Alex I.
5
@AlexI. De fato! Esta sintaxe cria um temporário, que é passado como argumento para 'emplace_back'. Perde completamente o ponto.
aldo
5
Eu não entendo todos os comentários negativos. O compilador não usará RVO neste caso?
Euri Pinhollow