Гарантирует ли стандарт C++, что неудачная вставка в ассоциативный контейнер не изменит аргумент rvalue-reference?

#include <set>
#include <string>
#include <cassert>

using namespace std::literals;

int main()
{
    auto coll = std::set{ "hello"s };
    auto s = "hello"s;
    coll.insert(std::move(s));
    assert("hello"s == s); // Always OK?
}

Гарантирует ли стандарт C++, что неудачная вставка в ассоциативный контейнер не изменит аргумент rvalue-reference?

Ответы

Ответ 1

Явный и однозначный НЕТ. Стандарт не имеет этой гарантии, и поэтому существует try_emplace.

Смотрите примечания:

В отличие от insert или emplace, эти функции не перемещаются из rvalue аргументы, если вставка не происходит, что облегчает манипулировать картами, значения которых являются типами только для перемещения, такими как std::map<std::string, std::unique_ptr<foo>>. Кроме того, try_emplace обрабатывает ключ и аргументы mapped_type отдельно, в отличие от emplace, который требует аргументов для построения value_type (что есть, std::pair)

Ответ 2

Номер

Хотя @NathanOliver указывает, что элемент не будет вставлен в том и только в том случае, если нет эквивалентного ключа, это не гарантирует, что аргументы не будут изменены.

Фактически, [map.modifiers] говорит следующее

template <class P>
pair<iterator, bool> insert(P&& x);

эквивалентно return emplace(std::forward<P>(x)).

Где emplace может идеально передать аргументы для создания другого P, оставив x в каком-то действительном, но неопределенном состоянии.

Вот пример, который также демонстрирует (не доказывает), что с std::map (ассоциативный контейнер) значение немного перемещается:

#include <iostream>
#include <utility>
#include <string>
#include <map>

struct my_class
{
    my_class() = default;
    my_class(my_class&& other)
    {
        std::cout << "move constructing my_class\n";
        val = other.val;
    }
    my_class(const my_class& other)
    {
        std::cout << "copy constructing my_class\n";
        val = other.val;
    }
    my_class& operator=(const my_class& other)
    {
        std::cout << "copy assigning my_class\n";
        val = other.val;
        return *this;
    }
    my_class& operator=(my_class& other)
    {
        std::cout << "move assigning my_class\n";
        val = other.val;
        return *this;
    }
    bool operator<(const my_class& other) const
    {
        return val < other.val;
    }
    int val = 0;
};

int main()
{
    std::map<my_class, int> my_map;
    my_class a;
    my_map[a] = 1;
    std::pair<my_class, int> b = std::make_pair(my_class{}, 2);
    my_map.insert(std::move(b)); // will print that the move ctor was called
}

Ответ 3

(Ответ только для С++ 17)

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

Как указывает AndyG, такая гарантия вообще не может существовать: иногда библиотека должна фактически выполнить конструкцию перемещения только для того, чтобы определить, возможна ли вставка. Это будет иметь место для функции emplace, поведение которой определяется стандартом как:

Эффекты: Вставляет объект value_type t, созданный с помощью std::forward<Args>(args)..., тогда и только тогда, когда в контейнере нет элемента с ключом, эквивалентным ключу t.

Мы можем интерпретировать это как утверждение, что объект t создается независимо от чего, а затем удаляется, если вставка не может произойти, потому что значение t или t.first уже существует в наборе или карте, соответственно. И поскольку метод template <class P> pair<iterator, bool> insert(P&&) из std::map определен в терминах emplace, как указывает AndyG, он имеет такое же поведение. Как указывает SergeyA, методы try_emplace предназначены для избежания этой проблемы.

Однако в конкретном примере, заданном OP, вставляемое значение имеет точно такой же тип, что и тип значения контейнера. Поведение такого вызова insert определяется параграфом общих требований, ранее заданным NathanOliver:

Эффекты: Вставляет t тогда и только тогда, когда в контейнере нет элемента с ключом, эквивалентным ключу t.

В этом случае не предоставляется лицензии для библиотеки на изменение аргумента в случае, когда вставка не происходит. Я полагаю, что вызов библиотечной функции не должен иметь каких-либо наблюдаемых побочных эффектов, кроме того, что явно разрешено стандартом. Таким образом, этот случай, t не должен быть изменен.