С ++ 11 emplace_back в векторе <struct>?

87

Рассмотрим следующую программу:

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

Не работает:

$ 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

Как правильно это сделать и почему?

(Также пробовали одинарные и двойные скобки)

Эндрю Томазос
источник
4
Это сработает, если вы предоставите соответствующий конструктор.
Крис
2
Есть ли способ создать его на месте с помощью автоматически созданного конструктора структуры скобок, используемого T t{42,3.14, "foo"}?
Эндрю Томазос
4
Я не думаю, что это принимает форму конструктора. Это совокупная инициализация.
Крис
5
Я никоим образом не пытаюсь повлиять на ваше мнение .. Но если вы какое-то время не уделяли внимания тонкому вопросу .. Принятый ответ, при полном уважении к его автору, вовсе не является ответом на ваш вопрос и может ввести читателей в заблуждение.
Humam Helfawi 08

Ответы:

18

Для всех в будущем это поведение будет изменено в C ++ 20 .

Другими словами, даже если внутренняя реализация по-прежнему будет вызывать, T(arg0, arg1, ...)она будет рассматриваться как обычная T{arg0, arg1, ...}, как и следовало ожидать.

Красный XIII
источник
93

Вам нужно явно определить ctor для класса:

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

Смысл использования emplace_back- избежать создания временного объекта, который затем копируется (или перемещается) в место назначения. Хотя также возможно создать временный объект, а затем передать его emplace_back, это нарушает (по крайней мере, большую часть) цель. Что вы хотите сделать, так это передать отдельные аргументы, а затем позволить emplace_backвызвать ctor с этими аргументами для создания объекта на месте.

Джерри Гроб
источник
12
Думаю, лучше было бы написатьT(int a, double b, string c) : a(a), b(b), c(std::move(c))
балки
9
Принятый ответ противоречит цели emplace_back. Это правильный ответ. Вот как emplace*работает. Они создают элемент на месте, используя перенаправленные аргументы. Следовательно, для получения указанных аргументов необходим конструктор.
underscore_d
1
тем не менее, вектор может предоставить emplace_aggr, верно?
tamas.kenez
@balki Правильно, нет смысла брать cего, &&если ничего не сделано с его возможной ценностью; в инициализаторе члена аргумент снова обрабатывается как lvalue в отсутствие приведения, поэтому член просто создается копией. Даже если член был сконструирован с перемещением, не идиоматично требовать от вызывающих всегда передавать временное или std::move()d lvalue (хотя я признаюсь, что в моем коде есть несколько угловых случаев, когда я это делаю, но только в деталях реализации) .
underscore_d
25

Конечно, это не ответ, но он показывает интересную особенность кортежей:

#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");
}
ричи
источник
8
Конечно, это не ответ. что в нем такого интересного? Таким способом можно установить любой тип с ctor. У кортежа есть ctor. структура op этого не сделала. вот ответ.
underscore_d
6
@underscore_d: Я не уверен, что помню каждую деталь того, о чем я думал 3 с половиной года назад, но я полагаю, что я предполагал, что если вы просто используете tupleвместо определения структуры POD, вы получите конструктор бесплатно , что означает, что вы получаете emplaceсинтаксис бесплатно (среди прочего - вы также получаете лексикографический порядок). Вы теряете имена участников, но иногда создавать аксессоры становится меньше, чем все остальное, что вам может понадобиться. Я согласен с тем, что ответ Джерри Коффина намного лучше принятого. Я тоже проголосовал за него много лет назад.
rici
3
Да, объяснение помогает мне понять, что вы имели в виду! Хорошая точка зрения. Я согласен с тем, что иногда такое обобщение можно сносить, если сравнить его с другими вещами, которые предоставляет нам STL: я использую это получасто с pair... но иногда задаюсь вопросом, действительно ли я много выиграю в чистом выражении, хех. Но, возможно tuple, последует в будущем. Спасибо за расширение!
underscore_d
12

Если вы не хотите (или не можете) добавлять конструктор, специализируйте распределитель для T (или создайте свой собственный распределитель).

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

Примечание. Показанная выше конструкция функции-члена не может быть скомпилирована с помощью clang 3.1 (извините, я не знаю почему). Попробуйте следующий, если вы будете использовать clang 3.1 (или по другим причинам).

void construct(T* p, int a, double b, const string& c) { ::new(static_cast<void*>(p)) T{ a, b, c }; }
Мицуру Кария
источник
В вашей функции выделения вам не нужно беспокоиться о выравнивании? Смотритеstd::aligned_storage
Эндрю Томазос
Нет проблем. В соответствии со спецификацией, эффекты «void * :: operator new (size_t size)» следующие: «Функция распределения, вызываемая выражением new, для выделения байтов размера памяти, подходящим образом выровненных для представления любого объекта такого размера».
Mitsuru Kariya
6

Кажется, это описано в 23.2.1 / 13.

Во-первых, определения:

Учитывая тип контейнера X, имеющий allocator_type, идентичный A, и value_type, идентичный T, и заданное lvalue m типа A, указатель p типа T *, выражение v типа T и rvalue rv типа T, определены следующие термины.

Теперь, что делает его возводимым на место:

T - EmplaceConstructible в X из args, для нуля или более аргументов args, означает, что следующее выражение правильно сформировано: allocator_traits :: construct (m, p, args);

И, наконец, примечание о реализации вызова конструкции по умолчанию:

Примечание. Контейнер вызывает allocator_traits :: construct (m, p, args) для создания элемента в точке p с использованием аргументов. Конструкция по умолчанию в std :: allocator будет вызывать :: new ((void *) p) T (args), но специализированные распределители могут выбрать другое определение.

Это в значительной степени говорит нам о том, что для схемы распределителя по умолчанию (и, возможно, единственной) вы должны определить конструктор с нужным количеством аргументов для того, что вы пытаетесь поместить-конструкцию в контейнер.

Марк Б
источник
-2

вам необходимо определить конструктор для вашего типа, Tпотому что он содержит std::stringнетривиальный.

кроме того, было бы лучше определить (возможно, по умолчанию) move ctor / assign (потому что у вас есть подвижный std::stringэлемент в качестве члена) - это поможет переместить ваш Tгораздо более эффективный ...

или просто используйте T{...}для вызова overloaded, emplace_back()как рекомендовано в ответе Neighboug ... все зависит от ваших типичных вариантов использования ...

Зауфи
источник
Конструктор перемещения создается автоматически для T
Эндрю Томазос
1
@ AndrewTomazos-Fathomling: только если пользовательские ctors не определены
zaufi
1
Правильно, а их нет.
Эндрю Томазос
@ AndrewTomazos-Fathomling: но вы должны определить некоторые, чтобы избежать временного emplace_back()вызова при вызове :)
zaufi
1
На самом деле неверно. Конструктор перемещения создается автоматически, если не определены деструктор, конструктор копирования или операторы присваивания. Определение поэлементного конструктора с 3 аргументами для использования с emplace_back не подавляет конструктор перемещения по умолчанию.
Эндрю Томазос
-2

Вы можете создать struct Tэкземпляр, а затем переместить его в вектор:

V.push_back(std::move(T {42, 3.14, "foo"}));
AlexB
источник
2
Вам не нужно std :: move () временный объект T {...}. Это уже временный объект (rvalue). Итак, вы можете просто отбросить std :: move () из своего примера.
Надав Хар'Эль,
Более того, даже имя типа T не обязательно - компилятор его угадывает. Так что просто "V.push_back {42, 3.14," foo "}" будет работать.
Надав Хар'Эль,
-8

Вы можете использовать {}синтаксис для инициализации нового элемента:

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

Это может или не может быть оптимизировано, но это должно быть.

Вы должны определить конструктор, чтобы это работало, обратите внимание, что с вашим кодом вы даже не можете:

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

Но это то, что вам нужно для работы на месте.

так что просто:

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

заставит его работать желаемым образом.

Perreal
источник
10
Будет ли это создавать временное, а затем перемещать его в массив? - или он создаст предмет на месте?
Эндрю Томазос
3
Это std::moveне обязательно. T{42, 3.14, "foo"}будет уже перенаправлен emplace_back и привязан к конструктору перемещения структуры как rvalue. Однако я бы предпочел решение, которое строит его на месте.
Эндрю Томазос
37
в этом случае перемещение почти точно эквивалентно копированию, поэтому упускается весь смысл размещения.
Alex I.
5
@AlexI. На самом деле! Этот синтаксис создает временный объект, который передается как аргумент emplace_back. Совершенно упускает суть.
aldo
5
Я не понимаю всех отрицательных отзывов. Не будет ли компилятор в этом случае использовать RVO?
Эури Пинхоллоу