Является ли std::move безопасным в списке аргументов, когда аргумент пересылается, а не создается перемещение?

Пытаясь предоставить решение для std::string_view и std::string в std::unordered_set, я играю с заменой std::unordered_set<std::string> на std::unordered_map<std::string_view, std::unique_ptr<std::string>> (значение равно std::unique_ptr<std::string>, потому что оптимизация небольшой строки будет означать, что адрес базовых данных string не всегда будет передаваться в результате std::move.

Мой исходный тестовый код, который, похоже, работает (без заголовков):

using namespace std::literals;

int main(int argc, char **argv) {
    std::unordered_map<std::string_view, std::unique_ptr<std::string>> mymap;

    for (int i = 1; i < argc; ++i) {
        auto to_insert = std::make_unique<std::string>(argv[i]);

        mymap.try_emplace(*to_insert, std::move(to_insert));
    }

    for (auto&& entry : mymap) {
        std::cout << entry.first << ": " << entry.second << std::endl;
    }

    std::cout << std::boolalpha << "\"this\" in map? " << (mymap.count("this") == 1) << std::endl;
    std::cout << std::boolalpha << "\"this\"s in map? " << (mymap.count("this"s) == 1) << std::endl;
    std::cout << std::boolalpha << "\"this\"sv in map? " << (mymap.count("this"sv) == 1) << std::endl;
    return EXIT_SUCCESS;
}

Я компилирую с g++ 7.2.0, строка компиляции g++ -O3 -std=c++17 -Wall -Wextra -Werror -flto -pedantic test_string_view.cpp -o test_string_view не получает никаких предупреждений, затем запускаю, получая следующий вывод:

$ test_string_view this is a test this is a second test
second: second
test: test
a: a
this: this
is: is
"this" in map? true
"this"s in map? true
"this"sv in map? true

чего я и ожидал.

Меня больше всего беспокоит следующее:

        mymap.try_emplace(*to_insert, std::move(to_insert));

имеет определенное поведение. *to_insert полагается на то, что to_insert не будет опустошен (путем создания std::unique_ptr, хранящегося на карте), до тех пор, пока не будет построен string_view. Два определения try_emplace, которые будут рассмотрены:

try_emplace(const key_type& k, Args&&... args);

а также

try_emplace(key_type&& k, Args&&... args);

Я не уверен, что будет выбрано, но в любом случае кажется, что key_type будет построено как часть вызова try_emplace, в то время как аргументы для создания mapped_type ("значение", хотя карты, кажется, используют value_type для ссылки на комбинированный ключ/значение pair) пересылаются вместе, а не используются сразу, что делает код определенным. Я прав в этой интерпретации, или это неопределенное поведение?

Меня беспокоит то, что другие подобные конструкции, которые кажутся определенно неопределенными, все еще работают, например:

mymap.insert(std::make_pair<std::string_view,
                            std::unique_ptr<std::string>>(*to_insert,
                                                          std::move(to_insert)));

производит ожидаемый результат, в то время как аналогичные конструкции, такие как:

mymap.insert(std::make_pair(std::string_view(*to_insert),
                            std::unique_ptr<std::string>(std::move(to_insert))));

запускать Segmentation fault во время выполнения, несмотря на то, что ни одна из них не вызывает никаких предупреждений, и обе конструкции, по-видимому, одинаково неупорядочены (неупорядоченные неявные преобразования в рабочем insert, непоследовательное явное преобразование в segfaulting insert), поэтому я не хочу говорить "try_emplace работал на меня, так что все в порядке».

Обратите внимание, что хотя этот вопрос похож на C++11: вызов std::move() для списка аргументов, он не совсем дубликат (это то, что предположительно делает std::make_pair здесь небезопасным, но не обязательно применимо к поведению, основанному на пересылке try_emplace); в этом вопросе функция, получающая аргументы, получает std::unique_ptr, немедленно запуская построение, в то время как try_emplace получает аргументы для пересылки, а не std::unique_ptr, поэтому, хотя std::move "произошло" (но еще ничего не сделало), я думаю мы в безопасности, так как std::unique_ptr построен "позже".


person ShadowRanger    schedule 08.08.2018    source источник


Ответы (1)


Да, ваш вызов try_emplace совершенно безопасен. std::move на самом деле ничего не перемещает, он просто приводит переданную переменную к xvalue. Независимо от того, в каком порядке инициализируются аргументы, ничто никогда не будет перемещено, потому что все параметры являются ссылками. Ссылки привязываются непосредственно к объектам, они не вызывают никаких конструкторов.

Если вы посмотрите на свой второй фрагмент, вы увидите, что std::make_pair также принимает свои параметры по ссылке, поэтому и в этом случае перемещение не будет выполнено, кроме как в теле конструктора.

Однако в вашем третьем фрагменте есть проблема с UB. Разница незначительна, но если аргументы make_pair оцениваются слева направо, то временный объект std::unique_ptr будет инициализирован перемещенным из значения to_insert. Это означает, что теперь to_insert имеет значение null, потому что перемещение действительно произошло, потому что вы явно создаете объект, который фактически выполняет перемещение.

person Rakete1111    schedule 08.08.2018
comment
Хотя это правда, я все же рекомендую быть очень осторожным с этим. Вы должны знать сигнатуру функции, которую вы вызываете, чтобы знать, что все ее аргументы являются ссылками, чтобы это было безопасно. И если позже автор изменит аргумент на значение, это уже не будет безопасно. Ничего не зная о сигнатуре функции, foo.frobnicate(*bar, std::move(bar)) поднимает красный флаг. - person Justin; 08.08.2018
comment
Второй фрагмент по-прежнему использует std::make_pair, а не конструктор std::pair напрямую, он просто явно указывает шаблонные типы (запускает неявное построение), а не явно создает, а затем передает. Разве это не должно быть столь же небезопасно? Даже получая по универсальной ссылке, он должен был бы создавать правильные типы, он просто перемещает время построения с времени непосредственно перед вызовом на время вызова, но любой из них все еще не упорядочивается, верно? Между загрузкой аргументов и преобразованием их в требуемые типы нет точки последовательности, не так ли? - person ShadowRanger; 08.08.2018
comment
@ShadowRanger Ой; Хотя это не имеет значения. Да, аргументы должны быть инициализированы, но, поскольку они являются ссылками, перемещение не происходит, если вы игнорируете тело. В самом деле, если вы замените make_pair функцией с той же сигнатурой, но с пустым телом, никакого перемещения сделано не будет. Больше нет точек последовательности, но да, каждый аргумент инициализируется, а затем следующий, ... Чередования или чего-то подобного нет. - person Rakete1111; 08.08.2018
comment
@ Rakete1111: А, я вижу свою ошибку. В общем, когда требуется преобразование, получение по ссылке по-прежнему требует построения. Но в этом случае требуется преобразование только первого аргумента, а не второго; он немедленно создает string_view из *to_insert, но просто передает ссылку xvalue на to_insert, не очищая ее; unique_ptr уже является правильным типом. Как и try_emplace, string_view создается сразу же как часть вызова std::make_pair, а unique_ptr создается позже. Спасибо! - person ShadowRanger; 08.08.2018