Как правильно пересылать unique_ptr?

Как обычно правильно пересылать std::unique_ptr?

В следующем коде используется std::move, что, по моему мнению, является общепринятой практикой, но происходит сбой с clang.

class C {
   int x;
}

unique_ptr<C> transform1(unique_ptr<C> p) {
    return transform2(move(p), p->x); // <--- Oops! could access p after move construction takes place, compiler-dependant
}

unique_ptr<C> transform2(unique_ptr<C> p, int val) {
    p->x *= val;
    return p;
}

Есть ли более надежное соглашение, чем просто убедиться, что вы получаете все, что вам нужно, от p, прежде чем передавать право собственности следующей функции через std::move? Мне кажется, что использование move для объекта и доступ к нему для предоставления параметра для одного и того же вызова функции может быть распространенной ошибкой.


person Danra    schedule 27.01.2016    source источник
comment
Вы когда-нибудь рассматривали возможность использования необработанных указателей? Именно в таких ситуациях так называемые «умные указатели» крайне непрактичны для использования.   -  person Poriferous    schedule 27.01.2016
comment
Этот код определенно не должен давать сбоев, по крайней мере, не по той причине, о которой вы упомянули — std::move на самом деле не выполняет перемещение, а p->x не обращается к p после перемещения.   -  person Konrad Rudolph    schedule 27.01.2016
comment
@KonradRudolph: все еще может произойти сбой. Инициализация параметра в transform2 будет выполнять построение перемещения. В этот момент p в transform1 пусто. Помните: компиляторы могут изменить порядок этих выражений так, как они считают нужным.   -  person Nicol Bolas    schedule 27.01.2016
comment
@NicolBolas Изменение порядка выражений само по себе не может вызвать сбой, потому что, как уже упоминалось, выражение std::move(p) ничего не делает, оно просто меняет статический тип выражения. Тем не менее, вы можете быть правы, потому что я не знаю, в какой именно момент времени происходит инициализация параметра; Я думал, что это произошло после вычисления всех выражений параметров, но понятия не имею.   -  person Konrad Rudolph    schedule 27.01.2016
comment
Зачем передавать p->x отдельно, если вы все равно передаете весь объект?   -  person Jonathan Potter    schedule 27.01.2016
comment
@JonathanPotter: предположительно, transform2 можно использовать со значением, отличным от p->x; transform2 — умножение общего назначения, transform1 — специализация, которая возводит в квадрат.   -  person ShadowRanger    schedule 27.01.2016
comment
@KonradRudolph, вы правы, порядок оценки не указан в стандарте, см. stackoverflow.com/questions/2934904/, таким образом, использование перемещения в любой точке программы приведет к тому, что любое использование этого объекта позже активирует UB.   -  person user1708860    schedule 27.01.2016
comment
Если вы не передаете право собственности, вы не должны передавать интеллектуальный указатель. Так или иначе, Никол Болас прибил это.   -  person T.C.    schedule 27.01.2016
comment
@user1708860 user1708860 На самом деле я говорю прямо противоположное: простое использование std::move не приводит к тому, что последующее использование этого объекта становится UB. Напротив, следующий код полностью соответствует стандарту: auto&& q = std::move(p); p->x;. У вас неправильное представление о том, чем занимается std::move.   -  person Konrad Rudolph    schedule 27.01.2016
comment
@KonradRudolph, посмотрев это, я понял, что ты имеешь в виду. Код действительно хорош, как есть - он не активирует UB. Если я правильно понимаю, это активирует неопределенное поведение, которое не приведет к сбою, но не обещает дать правильный результат.   -  person user1708860    schedule 27.01.2016
comment
@user1708860 user1708860 Код, который я показал в своем комментарии, четко определен: он не дает ни неопределенного, ни неопределенного поведения и всегда дает правильный результат, гарантировано. Однако, что касается кода в случае с ОП: см. комментарий Николя выше (и мой ответ на него).   -  person Konrad Rudolph    schedule 27.01.2016
comment
@KonradRudolph, можете ли вы процитировать стандарт или какой-либо другой ресурс?   -  person user1708860    schedule 27.01.2016
comment
@user1708860 en.cppreference.com/w/cpp/utility/move достаточно — обратите внимание на раздел «Возвращаемое значение». Это все, что делает std::move!   -  person Konrad Rudolph    schedule 27.01.2016
comment
Что было бы неплохо, так это компилятор, который предупреждает о возможном использовании перемещенных объектов так же, как и для неинициализированных переменных.   -  person Toby Speight    schedule 27.01.2016
comment
Комментарий в коде вопроса немного вводит в заблуждение. Проблема в том, что вы можете получить доступ к p после того, как произойдет построение перемещения параметра функции.   -  person David Schwartz    schedule 27.01.2016
comment
@DavidSchwartz Улучшенный комментарий, спасибо   -  person Danra    schedule 27.01.2016
comment
@NicolBolas Полностью ли оцениваются выражения до того, как происходит инициализация любого параметра, или можно ли изменить порядок выражений и инициализации параметров? Я подозреваю, что инициализация может быть переупорядочена относительно друг друга и выражений, но Конрад считает, что переупорядочения выражений недостаточно, я думаю.   -  person Yakk - Adam Nevraumont    schedule 27.01.2016
comment
@Yakk: можно ли изменить порядок инициализации выражений и параметров Я не знаю. Вообще говоря, когда речь идет о таких эзотерических правилах, как переупорядочивание, я ошибаюсь из-за осторожности.   -  person Nicol Bolas    schedule 27.01.2016


Ответы (6)


Поскольку вам действительно не нужно обращаться к p после его перемещения, один из способов — получить p->x перед перемещением, а затем использовать его.

Пример:

unique_ptr<C> transform1(unique_ptr<C> p) {
    int x = p->x;
    return transform2(move(p), x);
}
person SergeyA    schedule 27.01.2016
comment
Да, но вопрос в том, существует ли более надежное соглашение, чем просто убедиться, что вы получаете все, что вам нужно, от p, прежде чем передавать право собственности на следующую функцию через std::move, что вы и предлагаете. Идея состоит в том, чтобы предотвратить эту ошибку в целом. - person Danra; 27.01.2016
comment
@Данра, но ты не можешь. Вы не можете использовать содержимое указателя после того, как он был перемещен, и (как указано в комментариях), если вы используете std::move в контексте вызова функции, он не определен, когда объект будет перемещен. Однако в однопоточной программе вы можете получить необработанный указатель и использовать его в аргументах функции. Это то, что вы хотите получить? - person SergeyA; 27.01.2016
comment
Нет, я ищу способ предотвратить эту кажущуюся (по крайней мере для меня) простую ошибку. например Какая-то операция, похожая на перемещение, но которая сделает объект недействительным только после того, как он выйдет из области видимости (и до вызова его деструктора) - person Danra; 27.01.2016
comment
Но это всего лишь пример, я открыт для любого другого надежного решения, если оно существует. - person Danra; 27.01.2016
comment
@Danra, это не unique_ptr! Это делает объект недействительным сразу после перемещения, потому что это очень рационально для его существования. Если объект будет признан недействительным только тогда, когда он выйдет за пределы области действия, у вас будет более одного владельца (тот, который все еще находится в области действия, и тот, которому вы только что его передали). То, что вы говорите, похоже, используется для shared_ptr, а не для unique_ptr. - person SergeyA; 27.01.2016
comment
@Danra, если вы ищете фиктивный способ сделать это, вам следует поискать что-то более похожее на ответ Дэвида Хаима и удалить логику владения. - person user1708860; 27.01.2016
comment
@ user1708860, я совершенно не согласен с тем, что говорит Дэвид. Он эффективно выступает против семантики движения вообще. - person SergeyA; 27.01.2016
comment
@SergeyA Не совсем так, shared_ptr предлагает больше семантики, такой как копирование, которое увеличивает его счетчик. Требование оставаться «живым» до тех пор, пока не выйдет за рамки, является более слабым, чем то, что обеспечивает shared_ptr. - person Danra; 27.01.2016
comment
@ user1708860 Это упрощенный пример. Предположим, что семантика перемещения действительно требуется. Представленная проблема является общей. - person Danra; 27.01.2016

Код не подходит.

  • std::move — не что иное, как приведение (g++: что-то вроде static_cast<typename std::remove_reference<T>::type&&>(value))
  • Вычисление значения и побочные эффекты каждого выражения аргумента упорядочены до выполнения вызываемой функции.
  • Однако инициализация параметров функции происходит в контексте вызывающей функции. (Спасибо TC)

Цитаты из черновика N4296:

1.9/15 Выполнение программы

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

5.2.2/4 Вызов функции

При вызове функции каждый параметр (8.3.5) должен быть инициализирован (8.5, 12.8, 12.1) соответствующим аргументом. [Примечание: такие инициализации неопределенно упорядочены по отношению друг к другу (1.9) примечание] [...] Инициализация и уничтожение каждого параметра происходит в контексте вызывающей функции. [...]

Образец (г++ 4.8.4):

#include <iostream>
#include <memory>
struct X
{
    int x = 1;
    X() {}
    X(const X&) = delete;
    X(X&&) {}
    X& operator = (const X&) = delete;
    X& operator = (X&&) = delete;
};


void f(std::shared_ptr<X> a, X* ap, X* bp, std::shared_ptr<X> b){
    std::cout << a->x << ", " << ap << ", " << bp << ", " << b->x << '\n';
}

int main()
{
    auto a = std::make_unique<X>();
    auto b = std::make_unique<X>();
    f(std::move(a), a.get(), b.get(), std::move(b));
}

Вывод может быть 1, 0xb0f010, 0, 2, показывающим, что (нулевой) указатель сдвинут.

person Community    schedule 27.01.2016
comment
инициализация параметров функции не является выражением или оператором в теле вызываемой функции. Фактически, ваша собственная цитата говорит, что это происходит в контексте вызывающей функции, а не вызываемой функции. - person T.C.; 27.01.2016
comment
Обратите внимание, что возможный вывод должен читаться как 1, 0xb0f010, 0, 1, чтобы соответствовать отредактированному коду. Вы не можете редактировать 1 символ, поэтому этот комментарий служит проверкой здравомыслия для всех, кто читает. - person Mike Lui; 29.12.2018

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

Код не быстрее, если он весь в одной строке, и зачастую он менее корректен.

std::move — это изменяющая операция (или, точнее, она помечает операцию, которой следует следовать, как «мутировать нормально»). Он должен быть на отдельной строке или, по крайней мере, на строке без какого-либо другого взаимодействия с его параметром.

Это как foo( i++, i ). Вы что-то модифицировали и тоже использовали.

Если вам нужна универсальная безмозглая привычка, просто свяжите все аргументы в std::forward_as_tuple и вызовите std::apply для вызова функции.

unique_ptr<C> transform1(unique_ptr<C> p) {
  return std::experimental::apply( transform2, std::forward_as_tuple( std::move(p), p->x ) );
}

что позволяет избежать проблемы, потому что мутация p выполняется в строке, отличной от чтения адреса p.get() в p->x.

Или сверните свой собственный:

template<class F, class...Args>
auto call( F&& f, Args&&...args )
->std::result_of_t<F(Args...)>
{
  return std::forward<F>(f)(std::forward_as_tuple(std::forward<Args>)...);
}
unique_ptr<C> transform1(unique_ptr<C> p) {
  return call( transform2, std::move(p), p->x );
}

Цель здесь состоит в том, чтобы упорядочить выражения оценки параметра отдельно от инициализации оценки параметра. Он по-прежнему не устраняет некоторые проблемы, связанные с перемещением (например, SSO std::basic_string move-guts и проблемы со ссылкой на символ).

Кроме того, надеюсь, что компилятор добавит предупреждения для неупорядоченного mutate-and-read в общем случае.

person Yakk - Adam Nevraumont    schedule 27.01.2016
comment
Первое решение кажется мне немного наивным; Параметры rvalue-reference широко распространены, начиная с С++ 11, и как только вы связываете более одной такой функции, передающей параметр через цепочку, вы рискуете получить описанную выше ошибку, и это выглядит отлично, если всего одной строкой будет перенаправление на следующую функцию в цепочке, включая перемещение. Второе решение приятно, но не всем доступно, чтобы они не споткнулись о потенциальную ошибку. - person Danra; 27.01.2016
comment
@Danra Переадресация без доступа к данным так же безопасна, как и вызывающий абонент: если они не помечают мутацию одновременно с чтением, все должно быть в порядке. Пересылка с дополнительным доступом к данным требует проверки того, что то, что вы пересылаете, не является тем, что вы читаете в другом месте (поскольку пересылка - это возможный ход)? Можете ли вы построить явный контрпример? - person Yakk - Adam Nevraumont; 27.01.2016
comment
Явный контрпример чему? Не уверен, что понял. - person Danra; 27.01.2016
comment
@Danra Я утверждаю, что пересылка не приведет к таким ошибкам, потому что ошибка произойдет в точке, где пересылка была начата. С другой стороны, вы сказали, что цепочка форвардов каким-то образом увеличивает риск вышеупомянутой ошибки. Есть ли пример, когда это происходит глубоко в цепочке, но явно не на интерфейсе, где было выполнено перемещение? - person Yakk - Adam Nevraumont; 27.01.2016
comment
Первые два предложения Прекратите выполнять более одной операции в одной строке, если только операции не мутируют. Код не быстрее, если он весь в одной строке, и зачастую он менее корректен. этого ответа действительно суть проблемы imo. Другие ответы ходят вокруг да около. - person Chris Beck; 28.01.2016
comment
@Yakk Я просто имел в виду, что функция-нарушитель (transform1 в моем примере) могла сама получить свой параметр через конструктор перемещения, и это может стать более распространенным, поскольку переадресация становится более распространенным явлением в коде: разумно, что в цепочка пересылки параметров от одного к другому, причем одна из функций в середине совершает ошибку, например, в transform1. - person Danra; 31.01.2016

Как отмечено в ответе Дитера Люкинга, вычисления значений выполняются до тела функции, поэтому std::move и operator -> располагаются перед телом функции --- 1,9/15.

Однако это не указывает, что инициализация параметра выполняется после всех этих вычислений, они могут появляться где угодно по отношению друг к другу и к независимым вычислениям значений, если они выполняются до тело функции --- 5.2.2/4.

Это означает, что поведение здесь не определено, поскольку одно выражение изменяет p (переходя во временный аргумент), а другое использует значение p, см. https://stackoverflow.com/a/26911480/166389. Хотя, как упоминалось там, P0145 предлагает исправить порядок оценки слева направо (в данном случае). Это означало бы, что ваш код неисправен, но transform2(p->x, move(p)) будет делать то, что вы хотите. (Исправлено благодаря TC)

Что касается идиом, чтобы избежать этого, рассмотрите подход Дэвида Хейма с использованием unique_ptr<C> по ссылке, хотя это довольно непрозрачно для вызывающего абонента . Вы сигнализируете что-то вроде «Может изменить этот указатель». Состояние перемещения unique_ptr достаточно ясно, так что это вряд ли укусит вас так сильно, как если бы вы перешли от переданной ссылки на объект или что-то в этом роде.

В конце концов, вам нужна точка последовательности между использованием p и изменением p.

person TBBle    schedule 27.01.2016
comment
На самом деле, P0145 исправит это, чтобы сделать слева направо. - person T.C.; 28.01.2016

аммм ... не то чтобы ответ, а предложение - зачем вообще передавать право собственности на unique_ptr? кажется, что transformXXX играет с целочисленным значением, почему здесь должно играть роль управление памятью?

передать unique_ptr по ссылке:

unique_ptr<C>& transform1(unique_ptr<C>& p) {
    return transform2(p, p->x); // 
}

unique_ptr<C>& transform2(unique_ptr<C> p&, int val) {
    p->x *= val;
    return p;
}

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

person David Haim    schedule 27.01.2016
comment
Действительно, это упрощенный пример. В моем реальном случае мне нужно передать право собственности. - person Danra; 27.01.2016
comment
ваши функции не должны делать больше, чем одну хорошую вещь. передача права собственности и выполнение математических действий — это работа для двух функций, а не для одной. - person David Haim; 27.01.2016
comment
@DavidHaim, если бы то, что вы говорите, было бы правдой, не было бы необходимости в какой-либо семантике владения в любом умном указателе. - person SergeyA; 27.01.2016
comment
@SergeyA нет, я говорю, что должны быть функции, передающие память, и функции, выполняющие математические операции, а не то и другое одновременно. - person David Haim; 27.01.2016
comment
Это должно просто передать необработанный указатель, не являющийся владельцем (или observer_ptr). Если вы не касаетесь права собственности, вам все равно, кому принадлежит объект. - person T.C.; 27.01.2016

Вообще говоря, нет надежного способа сделать это, не заботясь о мелких деталях. Где это становится действительно подлым, так это в списках-инициализации-членов.

Просто в качестве примера, который идет на один шаг дальше вашего, что произойдет, если p->x сам является объектом, время жизни которого зависит от *p, а затем transform2(), который фактически не знает о временной связи между своими аргументами, передает val вперед к какой-либо функции приемника, не заботясь о том, чтобы сохранить *p в живых. И, учитывая его собственный масштаб, как он узнает, что должен?

Семантика перемещения — это лишь одна из функций, которыми можно легко злоупотребить, и с ними нужно обращаться осторожно. С другой стороны, это часть основного очарования C++: он требует внимания к деталям. В обмен на эту дополнительную ответственность это дает вам больше контроля, а наоборот?

person Yam Marcovic    schedule 27.01.2016
comment
Как только происходит построение перемещения, все внутри p, будь то POD или объект, будет недействительным, поэтому я не уверен, что понял суть вашего расширенного примера. Что касается философской части, я чувствую, что продемонстрированная ошибка может быть общей ошибкой, поэтому я надеялся, что есть более надежный способ ее избежать. Учитывая, что в настоящее время его нет, я надеюсь, что его можно будет добавить в std. - person Danra; 27.01.2016
comment
@Danra Дело в том, что даже если вы позаботились о получении p->x до вызова, то, в зависимости от того, что такое p->x , вы все равно можете столкнуться с ошибками. Это просто показало, что есть много деталей, которые необходимо учитывать, чтобы написать правильный C++. Я бы сказал, что std::move() это не то, что можно делать всегда. Скорее, его следует использовать, когда необходимо добиться корректности (сначала), а затем производительности. Это по своей сути опасный инструмент, поскольку он позволяет вам сохранить доступ к недопустимым объектам. - person Yam Marcovic; 27.01.2016
comment
Теперь я вижу вашу точку зрения. Но копирование/перемещение p-›x заранее, и при этом его время жизни все еще зависит от *p, звучит для меня так, как будто вы что-то спроектировали неправильно :) Однако копирование p-›x в параметр вызова функции, в то же время время, когда вы перемещаете сам p в другой параметр вызова функции, звучит так, как будто он должен работать - или, по крайней мере, выглядит так, как должен, в одной строке. - person Danra; 27.01.2016