C ++ 11 emplace_back auf vector <struct>?

87

Betrachten Sie das folgende Programm:

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

Es funktioniert nicht:

$ 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

Was ist der richtige Weg und warum?

(Auch versucht Einzel- und Doppelklammern)

Andrew Tomazos
quelle
4
Dies funktioniert, wenn Sie einen geeigneten Konstruktor angeben.
Chris
2
Gibt es eine Möglichkeit, es mit dem automatisch erstellten Klammerstrukturkonstruktor zu erstellen, der von verwendet wird T t{42,3.14, "foo"}?
Andrew Tomazos
4
Ich glaube nicht, dass das die Form eines Konstruktors hat. Es ist eine aggregierte Initialisierung.
Chris
5
Ich versuche nicht, Ihre Meinung in irgendeiner Weise zu beeinflussen. Aber für den Fall, dass Sie eine Weile nicht auf dünne Fragen geachtet haben. Die akzeptierte Antwort, mit vollem Respekt gegenüber ihrem Verfasser, ist überhaupt keine Antwort auf Ihre Frage und kann die Leser irreführen.
Humam Helfawi

Antworten:

18

Für jeden aus der Zukunft wird dieses Verhalten in C ++ 20 geändert .

Mit anderen Worten, obwohl die interne Implementierung immer noch aufgerufen wird T(arg0, arg1, ...), wird sie als regelmäßig angesehen T{arg0, arg1, ...}, wie Sie es erwarten würden.

Rot XIII
quelle
90

Sie müssen explizit einen Ctor für die Klasse definieren:

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

Der emplace_backZweck der Verwendung besteht darin, zu vermeiden, dass ein temporäres Objekt erstellt wird, das dann an das Ziel kopiert (oder verschoben) wird. Es ist zwar auch möglich, ein temporäres Objekt zu erstellen und dieses dann an zu übergeben emplace_back, es macht jedoch (zumindest den größten Teil) den Zweck zunichte. Sie möchten einzelne Argumente übergeben und dann emplace_backden ctor mit diesen Argumenten aufrufen, um das Objekt an Ort und Stelle zu erstellen.

Jerry Sarg
quelle
11
Ich denke, ein besserer Weg wäre zu schreibenT(int a, double b, string c) : a(a), b(b), c(std::move(c))
Balki
9
Die akzeptierte Antwort vereitelt den Zweck von emplace_back. Dies ist die richtige Antwort. So emplace*funktioniert es. Sie konstruieren das Element direkt anhand der weitergeleiteten Argumente. Daher wird ein Konstruktor benötigt, um diese Argumente zu übernehmen.
underscore_d
1
Trotzdem könnte der Vektor einen emplace_aggr liefern, oder?
Tamas.kenez
@balki Richtig, es hat keinen Sinn nehmen cdurch , &&wenn nichts mit ihren möglichen rvalueness getan wird; Beim Initialisierer des Mitglieds wird das Argument in Abwesenheit einer Besetzung erneut als Wert behandelt, sodass das Mitglied nur kopiert wird. Selbst wenn das Mitglied verschoben wurde, ist es nicht idiomatisch, von Anrufern zu verlangen, dass sie immer einen temporären oder std::move()d-Wert übergeben (obwohl ich zugeben werde, dass ich in meinem Code einige Eckfälle habe, in denen ich das mache, aber nur in Implementierungsdetails). .
underscore_d
25

Natürlich ist dies keine Antwort, aber es zeigt ein interessantes Merkmal von Tupeln:

#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
quelle
8
Es ist sicherlich keine Antwort. Was ist daran so interessant? Auf diese Weise kann jeder Typ mit einem Ctor eingelagert werden. Tupel hat einen Ctor. Die Struktur des Ops tat es nicht. Das ist die Antwort.
underscore_d
6
@underscore_d: Ich bin mir nicht sicher, ob ich mich an jedes Detail von dem erinnere, was ich vor dreieinhalb Jahren gedacht habe, aber ich habe vorgeschlagen, dass tupleSie einen Konstruktor kostenlos erhalten , wenn Sie nur eine anstelle einer POD-Struktur verwenden Dies bedeutet, dass Sie die emplaceSyntax kostenlos erhalten (unter anderem - Sie erhalten auch eine lexikografische Reihenfolge). Sie verlieren Mitgliedsnamen, aber manchmal ist es weniger mühsam, Accessoren zu erstellen als der gesamte Rest der Boilerplate, die Sie sonst benötigen würden. Ich stimme zu, dass Jerry Coffins Antwort viel besser ist als die akzeptierte. Ich habe es auch vor Jahren positiv bewertet.
Rici
3
Ja, wenn ich es buchstabiere, verstehe ich, was du meinst! Guter Punkt. Ich bin damit einverstanden, dass die Verallgemeinerung manchmal erträglich ist, wenn man sie gegen die anderen Dinge abwägt, die die STL für uns bereitstellt: Ich benutze das halb so oft mit pair... aber manchmal frage ich mich, ob ich wirklich netto viel gewinne, heh. Aber vielleicht tuplefolgt in Zukunft. Vielen Dank für die Erweiterung!
underscore_d
12

Wenn Sie keinen Konstruktor hinzufügen möchten (oder können), spezialisieren Sie den Allokator auf T (oder erstellen Sie Ihren eigenen Allokator).

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

Hinweis: Das oben gezeigte Elementfunktionskonstrukt kann nicht mit clang 3.1 kompiliert werden (Entschuldigung, ich weiß nicht warum). Versuchen Sie es als nächstes, wenn Sie Clang 3.1 (oder andere Gründe) verwenden.

void construct(T* p, int a, double b, const string& c) { ::new(static_cast<void*>(p)) T{ a, b, c }; }
Mitsuru Kariya
quelle
Müssen Sie sich in Ihrer Zuweisungsfunktion nicht um die Ausrichtung kümmern? Siehestd::aligned_storage
Andrew Tomazos
Kein Problem. Gemäß der Spezifikation sind die Auswirkungen von "void * :: operator new (size_t size)" "Die Zuweisungsfunktion, die von einem neuen Ausdruck aufgerufen wird, um Größenbytes des Speichers zuzuweisen, die geeignet ausgerichtet sind, um ein Objekt dieser Größe darzustellen."
Mitsuru Kariya
6

Dies scheint in 23.2.1 / 13 behandelt zu werden.

Erstens Definitionen:

Bei einem Containertyp X mit einem mit A identischen Allokator_Typ und einem mit T identischen Wertetyp und einem gegebenen Wert m vom Typ A, einem Zeiger p vom Typ T *, einem Ausdruck v vom Typ T und einem Wert rv vom Typ T ist der gegeben Folgende Begriffe sind definiert.

Was macht es nun emplace-konstruierbar:

T ist EmplaceConstructible in X aus Args. Für null oder mehr Argumente bedeutet Args, dass der folgende Ausdruck wohlgeformt ist: allocator_traits :: construct (m, p, args);

Und zum Schluss noch ein Hinweis zur Standardimplementierung des Konstruktaufrufs:

Hinweis: Ein Container ruft allocator_traits :: construct (m, p, args) auf, um mit args ein Element bei p zu erstellen. Das Standardkonstrukt in std :: allocator ruft :: new ((void *) p) T (args) auf, aber spezialisierte Allokatoren können eine andere Definition wählen.

Dies sagt uns ziemlich genau, dass Sie für ein Standard- Allokator- Schema (und möglicherweise das einzige Allokator-Schema) einen Konstruktor mit der richtigen Anzahl von Argumenten für das Objekt definiert haben müssen, das Sie in einen Container einfügen möchten.

Mark B.
quelle
-2

Sie müssen einen Konstruktor für Ihren Typ definieren, Tda dieser einen std::stringnicht trivialen enthält.

Darüber hinaus ist es besser, (möglicherweise standardmäßig) move ctor / assign zu definieren (da Sie ein bewegliches std::stringMitglied haben) - dies würde dazu beitragen, Ihr Tviel effizienteres zu verschieben ...

oder verwenden Sie einfach T{...}, um überlastet anzurufen, emplace_back()wie in der Nachbarantwort empfohlen ... alles hängt von Ihren typischen Anwendungsfällen ab ...

zaufi
quelle
Ein Verschiebungskonstruktor wird automatisch für T
Andrew Tomazos
1
@ AndrewTomazos-Fathomling: Nur wenn keine Benutzer ctors definiert sind
zaufi
1
Richtig, und das sind sie nicht.
Andrew Tomazos
@ AndrewTomazos-Fathomling: aber Sie müssen einige definieren, um vorübergehende Instanz auf emplace_back()Abruf zu vermeiden :)
zaufi
1
Eigentlich falsch. Ein Verschiebungskonstruktor wird automatisch generiert, sofern keine Destruktoren, Kopierkonstruktoren oder Zuweisungsoperatoren definiert sind. Das Definieren eines 3-Argument-Konstruktors für die Verwendung mit emplace_back unterdrückt den Standard-Verschiebungskonstruktor nicht.
Andrew Tomazos
-2

Sie können die struct TInstanz erstellen und dann in den Vektor verschieben:

V.push_back(std::move(T {42, 3.14, "foo"}));
AlexB
quelle
2
Sie müssen ein temporäres Objekt T {...} nicht std :: move (). Es ist bereits ein temporäres Objekt (rvalue). Sie können also einfach std :: move () aus Ihrem Beispiel entfernen.
Nadav Har'El
Darüber hinaus ist auch der Typname T nicht erforderlich - der Compiler kann es erraten. Es funktioniert also nur ein "V.push_back {42, 3.14," foo "}".
Nadav Har'El
-8

Mit der {}Syntax können Sie das neue Element initialisieren:

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

Dies kann optimiert werden oder nicht, sollte es aber sein.

Sie müssen einen Konstruktor definieren, damit dies funktioniert. Beachten Sie, dass Sie mit Ihrem Code nicht einmal Folgendes tun können:

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

Aber das ist es, was Sie brauchen, um Arbeit zu haben.

also nur:

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

wird es auf die gewünschte Weise funktionieren lassen.

perreal
quelle
10
Wird dies ein temporäres Konstrukt erstellen und es dann in das Array verschieben? - oder wird es den Gegenstand an Ort und Stelle konstruieren?
Andrew Tomazos
3
Das std::moveist nicht nötig. T{42, 3.14, "foo"}wird bereits von emplace_back weitergeleitet und als r-Wert an den struct move-Konstruktor gebunden. Ich würde jedoch eine Lösung bevorzugen, die sie an Ort und Stelle erstellt.
Andrew Tomazos
37
In diesem Fall entspricht das Verschieben fast genau dem Kopieren, sodass der gesamte Einlagerungspunkt übersehen wird.
Alex I.
5
@AlexI. Tatsächlich! Diese Syntax erstellt ein temporäres Element, das als Argument an 'emplace_back' übergeben wird. Verpasst den Punkt völlig.
Aldo
5
Ich verstehe nicht alle negativen Rückmeldungen. Wird der Compiler in diesem Fall kein RVO verwenden?
Euri Pinhollow