C ++ 11 embrace_back na wektorze <struct>?


87

Rozważ następujący program:

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

To nie działa:

$ 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

Jaki jest właściwy sposób, aby to zrobić i dlaczego?

(Wypróbowałem także pojedyncze i podwójne szelki)


4
To zadziała, jeśli dostarczysz odpowiedniego konstruktora.
chris

2
Czy istnieje sposób na zbudowanie go w miejscu za pomocą automatycznie utworzonego konstruktora struktury nawiasów klamrowych używanego przez T t{42,3.14, "foo"}?
Andrew Tomazos,

4
Nie sądzę, żeby przybierało to formę konstruktora. To inicjalizacja zbiorcza.
chris


5
Nie próbuję w żaden sposób wpływać na twoją opinię .. Ale gdybyś od jakiegoś czasu nie zwracał uwagi na cienkie pytanie .. Przyjęta odpowiedź, z pełnym szacunkiem dla jej autora, nie jest w ogóle odpowiedzią na twoje pytanie i może wprowadzić czytelników w błąd.
Humam Helfawi

Odpowiedzi:


18

Dla każdego z przyszłości to zachowanie zostanie zmienione w C ++ 20 .

Innymi słowy, nawet jeśli implementacja wewnętrzna nadal będzie nazywać T(arg0, arg1, ...)to, zostanie uznana za normalną T{arg0, arg1, ...}, której można się spodziewać.


93

Musisz jawnie zdefiniować ctora dla klasy:

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

Celem używania emplace_backjest uniknięcie tworzenia tymczasowego obiektu, który jest następnie kopiowany (lub przenoszony) do miejsca docelowego. Chociaż możliwe jest również utworzenie tymczasowego obiektu, a następnie przekazanie emplace_backgo, pokonuje on (przynajmniej większość) cel. To, co chcesz zrobić, to przekazać poszczególne argumenty, a następnie emplace_backwywołać ctora z tymi argumentami, aby utworzyć obiekt w miejscu.


12
Myślę, że lepszym sposobem byłoby pisanieT(int a, double b, string c) : a(a), b(b), c(std::move(c))
balki

9
Przyjęta odpowiedź mija się z celem emplace_back. To jest poprawna odpowiedź. Tak emplace*działa. Konstruują element na miejscu, używając przekazanych argumentów. Stąd potrzebny jest konstruktor, aby wziąć te argumenty.
underscore_d

1
mimo to wektor mógłby zapewnić embrace_aggr, prawda?
tamas.kenez

@balki Racja, nie ma sensu przejmować csię, &&jeśli nic nie zostanie zrobione z jego możliwą wartością r; przy inicjalizatorze składowej argument jest ponownie traktowany jako lwartość, w przypadku braku rzutowania, więc element członkowski jest po prostu konstruowany przez kopiowanie. Nawet jeśli element członkowski został skonstruowany jako ruch, nie jest idiomatyczne wymaganie, aby wywołujący zawsze przekazywali tymczasową lub std::move()d lwartość (chociaż przyznam się, że mam kilka przypadków narożnych w moim kodzie, w których to robię, ale tylko w szczegółach implementacji) .
underscore_d

25

Oczywiście nie jest to odpowiedź, ale pokazuje interesującą cechę krotek:

#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
To z pewnością nie jest odpowiedź. co w tym takiego interesującego? w ten sposób można umieścić dowolny typ z ktorem. tuple ma ktora. struktura operacyjna nie. to jest odpowiedź.
underscore_d

6
@underscore_d: Nie jestem pewien, czy pamiętam każdy szczegół tego, o czym myślałem 3½ lat temu, ale sądzę, że sugerowałem, że jeśli użyjesz po prostu tuplezamiast definiowania struktury POD, otrzymasz konstruktora za darmo co oznacza, że ​​otrzymujesz emplaceskładnię za darmo (między innymi - otrzymujesz też porządek leksykograficzny). Tracisz nazwy członków, ale czasami tworzenie akcesorów jest mniej kłopotliwe niż cała reszta standardowego szablonu, którego w innym przypadku potrzebowałbyś. Zgadzam się, że odpowiedź Jerry'ego Coffina jest znacznie lepsza niż ta przyjęta. Głosowałem też za tym lata temu.
rici

3
Tak, przeliterowanie pomaga mi zrozumieć, o co ci chodziło! Słuszna uwaga. Zgadzam się, że czasami uogólnienie jest do zniesienia w porównaniu z innymi rzeczami, które zapewnia nam STL: używam tego półczęsto z pair... ale czasami zastanawiam się, czy naprawdę dużo zyskam w ujęciu netto, heh. Ale być może tuplenastąpi to w przyszłości. Dzięki za rozszerzenie!
underscore_d

12

Jeśli nie chcesz (lub nie możesz) dodawać konstruktora, wyspecjalizuj alokator dla T (lub utwórz własny alokator).

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

Uwaga: Konstrukcja funkcji składowej pokazana powyżej nie może skompilować się z clang 3.1 (przepraszam, nie wiem dlaczego). Wypróbuj następny, jeśli będziesz używać clang 3.1 (lub z innych powodów).

void construct(T* p, int a, double b, const string& c) { ::new(static_cast<void*>(p)) T{ a, b, c }; }

Czy w funkcji przydzielania nie musisz się martwić o wyrównanie? Zobaczstd::aligned_storage
Andrew Tomazos

Nie ma problemu. Zgodnie ze specyfikacją, efektem "void * :: operator new (rozmiar_t rozmiar)" jest "Funkcja alokacji wywoływana przez nowe wyrażenie w celu przydzielenia rozmiaru bajtów pamięci odpowiednio wyrównanych do reprezentowania dowolnego obiektu o tym rozmiarze."
Mitsuru Kariya

6

Wydaje się, że jest to uwzględnione w 23.2.1 / 13.

Po pierwsze, definicje:

Biorąc pod uwagę typ kontenera X mający alokator_type identyczny z A i value_type identyczny z T i mając podaną lwartość m typu A, wskaźnik p typu T *, wyrażenie v typu T i rvalue rv typu T, zdefiniowano następujące terminy.

Teraz, co sprawia, że ​​jest to możliwe do zbudowania:

T jest EmplaceConstructible do X z args, dla zera lub większej liczby argumentów args, oznacza, że ​​następujące wyrażenie jest poprawnie sformułowane: Alokator_traits :: construct (m, p, args);

I na koniec uwaga na temat domyślnej implementacji wywołania konstrukcji:

Uwaga: Kontener wywołuje przydzielator_traits :: construct (m, p, args), aby skonstruować element w p przy użyciu args. Konstrukcja domyślna w std :: przydzielacz wywoła :: new ((void *) p) T (args), ale wyspecjalizowane alokatory mogą wybrać inną definicję.

To prawie mówi nam, że dla domyślnego (i potencjalnie jedynego) schematu alokatora musisz zdefiniować konstruktor z odpowiednią liczbą argumentów dla rzeczy, którą próbujesz umieścić w kontenerze.


-2

musisz zdefiniować konstruktora dla swojego typu, Tponieważ zawiera on plikstd::string który nie jest trywialny.

co więcej, byłoby lepiej zdefiniować (możliwe domyślne) przeniesienie ctor / assign (ponieważ masz ruchomy std::stringjako członek) - pomogłoby to przenieśćT znacznie wydajniej ...

lub po prostu użyj T{...}do wywołania przeciążonego, emplace_back()zgodnie z zaleceniami w odpowiedzi sąsiada ... wszystko zależy od typowych przypadków użycia ...


Konstruktor ruchu jest generowany automatycznie dla T
Andrew Tomazos,

1
@ AndrewTomazos-Fathomling: tylko wtedy, gdy nie jest wskazana żadna osoba odpowiedzialna za użytkownika
zaufi

1
Prawda, ale nie są.
Andrew Tomazos,

@ AndrewTomazos-Fathomling: ale niektóre musisz zdefiniować, aby uniknąć tymczasowego emplace_back()wezwania :)
zaufi

1
Właściwie niepoprawne. Konstruktor przenoszenia jest generowany automatycznie pod warunkiem, że nie zdefiniowano żadnego destruktora, konstruktora kopiującego ani operatorów przypisania. Zdefiniowanie 3-argumentowego konstruktora składowego do użytku z embrace_back nie spowoduje pomijania domyślnego konstruktora przenoszenia.
Andrew Tomazos,

-2

Możesz utworzyć struct Tinstancję, a następnie przenieść ją do wektora:

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

2
Nie musisz std :: move () tymczasowego obiektu T {...}. To już obiekt tymczasowy (rvalue). Możesz więc po prostu usunąć std :: move () z przykładu.
Nadav Har'El

Co więcej, nawet nazwa typu T nie jest konieczna - kompilator może to odgadnąć. Więc tylko „V.push_back {42, 3.14,„ foo ”}” będzie działać.
Nadav Har'El

-8

Możesz użyć {}składni, aby zainicjować nowy element:

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

Może to być zoptymalizowane lub nie, ale powinno być.

Musisz zdefiniować konstruktora, aby to działało, pamiętaj, że z twoim kodem nie możesz nawet zrobić:

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

Ale to jest to, czego potrzebujesz, aby mieć pracę w miejscu pracy.

więc tylko:

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

sprawi, że będzie działać w pożądany sposób.


10
Czy to skonstruuje obiekt tymczasowy, a następnie przeniesie go do tablicy? - czy też skonstruuje przedmiot w miejscu?
Andrew Tomazos,

3
Nie std::movejest to konieczne. T{42, 3.14, "foo"}zostanie już przekazana przez embrace_back i powiązana z konstruktorem przenoszenia struktury jako rvalue. Wolałbym jednak rozwiązanie, które konstruuje je na miejscu.
Andrew Tomazos,

37
w tym przypadku ruch jest prawie dokładnie równoważny z kopiowaniem, więc cały punkt umieszczania jest pomijany.
Alex I.

5
@AlexI. W rzeczy samej! Ta składnia tworzy tymczasowy, który jest przekazywany jako argument do „embrace_back”. Całkowicie mija się z celem.
aldo

5
Nie rozumiem wszystkich negatywnych opinii. Czy kompilator nie użyje w tym przypadku RVO?
Euri Pinhollow
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.