С++ 17 порядок оценки выражения и std:: move

Во время рефакторинга некоторого кода сегодня, чтобы изменить исходные указатели на std::unique_ptr, я столкнулся с ошибкой сегментации из-за ошибки порядка оценки.

Старый код сделал примерно следующее:

void add(const std::string& name, Foo* f)
{
    _foo_map[name] = f;
}

void process(Foo* f)
{
    add(f->name, f);
}

Первый, наивный, рефакторинг кода для использования std::unique_ptr:

void add(const std::string& name, std::unique_ptr<Foo> f)
{
    _foo_map[name] = std::move(f);
}

void process(std::unique_ptr<Foo> f)
{
    add(f->name, std::move(f)); // segmentation-fault on f->name
}

Рефакторизованный код вызывает ошибку сегментации, потому что сначала обрабатывается второй аргумент (std::move(f)), а затем 1-й аргумент (f->name) разделяет перемещенную переменную, стрелу!

Возможными исправлениями являются получение дескриптора на Foo::name, прежде чем переместить его в вызов add:

void process(std::unique_ptr<Foo> f)
{
    const std::string& name = f->name;
    add(name, std::move(f));
}

Или, возможно:

void process(std::unique_ptr<Foo> f)
{
    Foo* fp = f.get();
    add(fp->name, std::move(f));
}

Оба этих решения требуют дополнительных строк кода и не кажутся почти такими же композитными, как и исходный (хотя и UB) вызов add.

Вопросы:

  • Являются ли какие-либо из 2 предложенных решений выше идиоматического С++, а если нет, есть ли лучшая альтернатива?

  • Я вижу, что есть перемены, идущие на С++ 17 из-за P0145R3 - Уточнение оценки выражения для Идиоматический С++. Изменит ли это какое-либо из вышеупомянутых решений/предотвратит их подводные камни?

Ответ 1

Для меня эти 2 предложения выглядят плохо. В любом из них вы отдаете свой объект Foo прочь. Это означает, что вы больше не можете делать какие-либо предположения об этом состоянии после этого. Он может быть освобожден внутри функции add до того, как будет обработан первый аргумент (ссылка на строку или указатель на объект). Да, он будет работать в текущей реализации, но он может сломаться, как только кто-либо коснется реализации add или чего-то более глубокого в нем.

Безопасные способы:

  • Сделайте копию строки и передайте ее как первый аргумент add
  • Рефакторируйте ваш метод add, чтобы он использовал только один аргумент типа Foo и извлекал Foo::name внутри метода и не воспринимал его как аргумент. Однако у вас все еще есть такая же проблема внутри метода add.

Во втором подходе вы можете обойти проблему порядка оценки, сначала создав новую запись карты (со значением по умолчанию) и получив изменяемую ссылку на нее и присвоив значение впоследствии:

auto& entry = _foo_map[f->name];
entry = std::move(f);

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

Если я снова подумаю об этом, вы также можете пойти по принципу "копировать имя". В любом случае его нужно скопировать для ключа карты. Если вы скопируете его вручную, вы можете переместить его для ключа, нет накладных расходов.

std::string name = f->name;
_foo_map[std::move(name)] = std::move(f);

Edit:

Как указано в комментариях, должно быть возможно напрямую назначить _foo_map[f->name] = std::move(f) внутри функции add, так как здесь гарантируется порядок оценки.

Ответ 2

Вы можете сделать add с помощью f по ссылке, что позволяет избежать ненужной копии/перемещения (тот же экземпляр/перемещение, который разбивает f->name):

void add(const std::string& name, std::unique_ptr<Foo> && f)
{
    _foo_map[name] = std::move(f);
}

void process(std::unique_ptr<Foo> f)
{
    add(f->name, std::move(f));
}

foo_map[name] должен быть оценен до того, как на него будет вызываться operator=, поэтому даже если name ссылается на что-то в зависимости от f, проблем нет.