Принимать аргументы и перемещать семантику для функций, которые могут дать сбой (надежная безопасность исключений)

У меня есть функция, которая работает с большим куском данных, переданным в качестве аргумента приемника. Мой тип BigData уже поддерживает C++11 и поставляется с полнофункциональным конструктором перемещения и реализациями присваивания перемещения, так что я могу уйти без необходимости копировать эту чертову штуку:

Result processBigData(BigData);

[...]

BigData b = retrieveData();
Result r = processBigData(std::move(b));

Все это прекрасно работает. Однако моя функция обработки может время от времени давать сбой во время выполнения, что приводит к исключению. На самом деле это не проблема, так как я могу просто исправить вещи и повторить попытку:

BigData b = retrieveData();
Result r;
try {
    r = processBigData(std::move(b));
} catch(std::runtime_error&) {
    r = fixEnvironmnentAndTryAgain(b);
    // wait, something isn't right here...
}

Конечно, это не сработает.

Поскольку я переместил свои данные в функцию обработки, к тому времени, когда я попаду в обработчик исключений, b уже нельзя будет использовать.

Это угрожает резко снизить мой энтузиазм по поводу передачи аргументов стока по значению.

Итак, вот вопрос: как справиться с такой ситуацией в современном коде C++? Как получить доступ к данным, которые ранее были перемещены в функцию, которую не удалось выполнить?

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


person ComicSansMS    schedule 04.09.2014    source источник
comment
Важный вопрос: содержит ли Result перемещенные ресурсы b или он просто основан на нем?   -  person IdeaHat    schedule 04.09.2014
comment
@MadScienceDreams Result просто вычисляется из b, оно не содержит ссылку или копию исходного b.   -  person ComicSansMS    schedule 04.09.2014
comment
@ComicSansMS Но содержит ли он перемещенное (а не скопированное) содержимое?   -  person Potatoswatter    schedule 04.09.2014
comment
Тогда нет причин проходить мимо rhr. Всякий раз, когда вы вызываете std::move, вы соглашаетесь с тем, что значение исчезнет после вызова функции. Хотя могут быть уловки, чтобы обойти это (не гарантируется, что оно исчезнет, ​​просто вы согласились, что оно может исчезнуть), правильный способ передать значение, на которое вы не хотите иметь побочных эффектов (даже в современных c++) является константной ссылкой.   -  person IdeaHat    schedule 04.09.2014
comment
@Potatoswatter Если это поможет вам решить проблему, не стесняйтесь предполагать, что это так. Однако в моем коде в его нынешнем виде b отбрасывается функцией обработки, как только она возвращается.   -  person ComicSansMS    schedule 04.09.2014
comment
@MadScienceDreams В моей конкретной ситуации для прохождения const& мне потребовалось бы скопировать все данные (на самом деле я столкнулся с этой проблемой в функции асинхронной обработки, поэтому право собственности на данные действительно нужно передать функции). Лучшее решение, которое я мог бы придумать, чтобы избежать копирования, - это использовать shared_ptr и пройти через кучу, но я бы хотел избежать этого, если это возможно.   -  person ComicSansMS    schedule 04.09.2014
comment
@ComicSansMS Итак, результат не потребляет ресурсы BigData, а побочный эффект функции? Я не думаю, что вы можете обойти использование общего указателя для такого неоднозначного владения... (Поскольку у BigData есть ресурсы, которые можно ускорить за счет перемещения, я предполагаю, что он уже использует ресурсы кучи).   -  person IdeaHat    schedule 04.09.2014
comment
@MadScienceDreams В значительной степени. Хотя я не вижу, как тот факт, кто в конечном итоге потребляет ресурс, меняет результат. Предполагая, что он будет поглощен результатом, позволит ли это найти более приятное решение?   -  person ComicSansMS    schedule 04.09.2014
comment
@ComicSansMS Нет, просто объясните, что происходит. Одним из возможных (но безумных) решений было бы иметь выходную переменную с (потенциально) заполненным объектом. Result processBigData(BigData&& in_ref, BigData* out_ref=NULL){BigData whereitmoves(std::foreward(in_ref));try{/*old method*/}catch(...){if (out_ref){*out_ref=std::move(whereitmoves);} std::rethrow_exception(std::current_exception());}   -  person IdeaHat    schedule 04.09.2014


Ответы (2)


Я так же озадачен этим вопросом.

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

template< typename t >
std::decay_t< t >
val( t && o ) // Given an object, return a new object "val"ue by move or copy
    { return std::forward< t >( o ); }

Result processBigData(BigData && in_rref) {
    // implementation
}

Result processBigData(BigData const & in_cref ) {
    return processBigData( val( in_cref ) );
}

Конечно, фрагменты аргумента могли быть перемещены перед исключением. Проблема распространяется на любые вызовы processBigData.

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

person Potatoswatter    schedule 04.09.2014
comment
Я все еще не уверен, почему вы хотите передать RHR для функции, если она на самом деле не использует BigData... Я думаю, чтобы позволить ей использовать ее в будущем? Кроме того, что вы делаете в случае некопируемого типа (например, std::unique_ptr)? - person IdeaHat; 04.09.2014
comment
Да, добавление перегрузки для ссылок rvalue действительно помогло бы здесь. Вы можете отложить переход от BigData до точки, в которой вы можете гарантировать, что исключений не возникнет. Конечно, это страдает от обычных недостатков, связанных с необходимостью вводить перегрузки rvalue-ref для интерфейсов функций (поэтому я не на 100% уверен, что это удовлетворяет моему ограничению удобства использования), но в некоторых ситуациях это может работать нормально. +1 в любом случае. - person ComicSansMS; 04.09.2014
comment
@MadScienceDreams 1. Да, учитывая поясняющие комментарии к вопросу, я не уверен, почему это не const &, но я просто концептуально ответил на вопрос. 2. Некопируемые типы не нуждаются в перегрузке const &, вот и все. - person Potatoswatter; 04.09.2014

Судя по всему, этот вопрос активно обсуждался на недавней конференции CppCon 2014. Херб Саттер резюмировал последние новости в своем заключительном докладе Назад к основам! Основы современного стиля C++ (слайды).

Его вывод довольно прост: Не используйте передачу по значению для аргументов приемника.

Аргументы в пользу использования этого метода в первую очередь (как популяризировалось Эриком Ниблером на конференции Meeting C++ 2013, Дизайн библиотеки C++11 (слайды)), кажется, перевешивают недостатки. Первоначальная мотивация для передачи аргументов приемника по значению состояла в том, чтобы избавиться от комбинаторного взрыва для перегрузок функций, который возникает в результате использования const&/&&.

К сожалению, кажется, что это влечет за собой ряд непредвиденных последствий. Одним из них являются потенциальные недостатки эффективности (в основном из-за ненужного выделения буфера). Другая проблема с безопасностью исключений из этого вопроса. Оба они обсуждаются в докладе Херба.

Херб пришел к выводу, что не использовать передачу по значению для аргументов приемника, а вместо этого полагаться на отдельные const&/&& (при этом const& используется по умолчанию, а && зарезервировано для тех немногих случаев, когда требуется оптимизация).

Это также соответствует тому, что предложил ответ @Potatoswatter. Передавая аргумент приемника через &&, мы могли бы отложить фактическое перемещение данных из аргумента до момента, когда мы можем дать гарантию отсутствия исключений.

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

Обновление после пяти лет размышлений об этом:

Теперь я убежден, что мой мотивирующий пример — неправильное использование семантики ходов. После вызова processBigData(std::move(b)); мне ни в коем случае нельзя позволять предположить, в каком состоянии находится b, даже если функция завершается с исключением. Это приводит к коду, за которым трудно следить и поддерживать.

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

class BigDataException : public std::runtime_error {
private:
    BigData b;
public:
    BigData retrieveDataAfterError() &&;

    // [...]
};


BigData b = retrieveData();
Result r;
try {
    r = processBigData(std::move(b));
} catch(BigDataException& e) {
    b = std::move(e).retrieveDataAfterError();
    r = fixEnvironmnentAndTryAgain(std::move(b));
}

Если я хочу восстановить содержимое b, мне нужно явно передать его по пути ошибки (в данном случае завернуто внутрь BigDataException). Этот подход требует немного дополнительного шаблона, но он более идиоматичен, поскольку не требует делать предположения о состоянии перемещаемого объекта.

person ComicSansMS    schedule 17.09.2014