C ++ 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
Я жодним чином не намагаюся вплинути на вашу думку .. Але якщо ви з певного часу не звертали уваги на тонке запитання .. Прийнята відповідь, з повною повагою до її автора, зовсім не є відповіддю на ваше запитання і може ввести читачів в оману.
Хумам Хельфаві,

Відповіді:


18

Для будь-кого з майбутнього ця поведінка буде змінена в C ++ 20 .

Іншими словами, навіть незважаючи на те, що впровадження всередині буде все одно викликати, T(arg0, arg1, ...)це буде вважатися регулярним, T{arg0, arg1, ...}що ви очікуєте.


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))
balki

9
Прийнята відповідь перемагає мету emplace_back. Це правильна відповідь. Ось як emplace*працює. Вони конструюють елемент на місці, використовуючи переслані аргументи. Отже, для прийняття зазначених аргументів потрібен конструктор.
underscore_d

1
все-таки вектор може надати emplace_aggr, так?
tamas.kenez

@balki Правильно, немає сенсу прийматися c, &&якщо нічого не робити з його можливою оцінкою r; при ініціалізації члена аргумент знову обробляється як значення, за відсутності приведення, тому член просто отримує копію. Навіть якщо учасник був побудований з переміщенням, не ідіоматично вимагати, щоб абоненти завжди передавали тимчасове значення або std::move()значення (хоча я визнаю, що в моєму коді є кілька наріжних випадків, де я це роблю, але лише в деталях реалізації) .
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 може бути встановлений таким чином. кортеж має ктор. структура опера не зробила. ось відповідь.
underscore_d

6
@underscore_d: Я не впевнений, що пам'ятаю кожну деталь того, про що думав 3½ роки тому, але я вважаю, що я пропонував, що якщо ви просто використовуєте tupleзамість того, щоб визначати структуру POD, то ви отримуєте конструктор безкоштовно , що означає, що ви отримуєте emplaceсинтаксис безкоштовно (серед іншого - ви також отримуєте лексикографічне впорядкування). Ви втрачаєте імена членів, але іноді це менше турбує створення аксесуарів, ніж усі інші типи шаблону, які вам потрібні в іншому випадку. Я згоден, що відповідь Джеррі Коффіна набагато краща за прийняту. Я також підтримав його багато років тому.
Річі

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 (Вибачте, я не знаю чому). Спробуйте наступний, якщо ви будете використовувати кланг 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-виразом для розподілу байтів розміру сховища, відповідно вирівняних для представлення будь-якого об'єкта такого розміру."
Міцуру Карія

6

Здається, це висвітлено у 23.2.1 / 13.

По-перше, визначення:

Дано контейнер типу X, який має тип_розподілювача, ідентичний A, і тип_значення, ідентичний T, і дане значення l типу A, вказівник p типу T *, вираз v типу T та значення rv rv типу T, визначені наступні терміни.

Тепер, що робить його зручним для побудови:

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

І, нарешті, примітка про реалізацію виклику конструкції за замовчуванням:

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

Це майже говорить нам про те, що для схеми розподілювача за замовчуванням (і, можливо, єдиної) ви повинні визначити конструктор із відповідною кількістю аргументів для того, що ви намагаєтесь замістити-побудувати у контейнер.


-2

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

крім того, було б краще визначити (можливо, за замовчуванням) переміщення ctor / assign (оскільки у вас є рухомий std::stringчлен) - це допомогло б перенести ваш Tнабагато ефективніший ...

або просто використовуйте T{...}для виклику перевантаження, emplace_back()як рекомендується у відповіді сусіда ... все залежить від типових випадків використання ...


Конструктор переміщення автоматично генерується для Т
Ендрю Томазоса

1
@ AndrewTomazos-Fathomling: лише якщо не визначено жодного користувацького
ктора

1
Правильно, а вони ні.
Ендрю Томазос,

@ AndrewTomazos-Fathomling: але вам потрібно визначити деякі, щоб уникнути тимчасового екземпляра за emplace_back()викликом :)
zaufi

1
Насправді неправильно. Конструктор переміщення створюється автоматично, за умови, що не визначено деструктор, конструктор копіювання або оператори присвоєння. Визначення 3-аргументного конструктора з використанням emplace_back не пригнічує конструктор переміщення за замовчуванням.
Ендрю Томазос,

-2

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

V.push_back(std::move(T {42, 3.14, "foo"}));

2
Вам не потрібно std :: move () тимчасовий об'єкт T {...}. Це вже тимчасовий об'єкт (rvalue). Тож ви можете просто скинути std :: move () з вашого прикладу.
Nadav Har'El

Більше того, навіть ім'я типу T не є необхідним - компілятор може це вгадати. Отже, просто "V.push_back {42, 3.14," foo "}" буде працювати.
Nadav Har'El

-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_) {}
}

змусить це працювати бажаним чином.


10
Чи буде це створювати тимчасове, а потім переміщувати його в масив? - або він сконструює предмет на місці?
Ендрю Томазос,

3
std::moveНе потрібно. T{42, 3.14, "foo"}вже буде переадресовано emplace_back і прив'язане до конструктора переміщення struct як значення r. Однак я вважаю за краще рішення, яке створює його на місці.
Ендрю Томазос,

37
у цьому випадку переміщення майже точно еквівалентно копії, тому вся суть заміщення пропущена.
Олексій І.

5
@AlexI. Справді! Цей синтаксис створює тимчасовий, який передається як аргумент 'emplace_back'. Повністю пропускає суть.
aldo

5
Я не розумію всіх негативних відгуків. Чи не використовуватиме компілятор RVO у цьому випадку?
Euri Pinhollow,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.