2016-01-27 5 views
9

Что такое правильный способ перенаправления std :: unique_ptr?Как правильно переслать 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 и доступ к нему для обеспечения параметра для того же вызова функции может быть распространенной ошибкой.

+3

Вы когда-нибудь рассматривали использование необработанных указателей? Это такие ситуации, когда так называемые «умные указатели» крайне непрактичны для использования. – Poriferous

+4

Этот код определенно не должен сбой, по крайней мере, не по той причине, о которой вы говорите: 'std :: move' фактически не выполняет перемещение, а' p-> x' does * not * access 'p' после перемещения , –

+14

@ KonradRudolph: Он все равно может потерпеть неудачу. Инициализация параметра 'transform2' * будет * выполнять конструкцию перемещения. В этот момент 'p' в' transform1' пуст. Помните: компиляторы могут переупорядочить эти выражения так, как они считают нужным. –

ответ

5

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

Пример:

unique_ptr<C> transform1(unique_ptr<C> p) { 
    int x = p->x; 
    return transform2(move(p), x); 
} 
+0

Да, но вопрос в том, «Есть ли более надежное соглашение», чем просто убедиться, что вы получите все, что вам нужно, от p до передачи права собственности на следующую функцию через std :: move », что вы предлагаете. Идея состоит в том, чтобы предотвратить эту ошибку в целом. – Danra

+2

@ Данра, но вы не можете. Вы не можете использовать содержимое указателя после его перемещения и (как указано в комментариях), если вы используете 'std :: move' в контексте вызова функции, он не определен, когда объект будет перемещен. Однако в однопоточной программе вы можете получить необработанный указатель и использовать его в аргументах функции. Это то, что вы хотите получить? – SergeyA

+0

Нет, я ищу способ, который мог бы помешать этому, казалось бы, (по крайней мере, мне) легко сделать ошибку. например Какая-то операция, подобная перемещению, но которая приведет к недействительности объекта после его выхода из области видимости (и до вызова ее деструктора) – Danra

1

Ammm ... не так много ответа, но на предложение - почему передать право собственности на 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; 
} 

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

+0

Действительно, это упрощенный пример. В моем реальном случае мне нужно передать право собственности. – Danra

+0

ваши функции не должны делать больше, чем одна хорошая вещь. передача собственности и выполнение математики - это работа для двух функций, а не одна. –

+1

@DavidHaim, если то, что вы говорите, было бы правдой, не было бы никакой необходимости в какой-либо семантике владения внутри любого умного указателя. – SergeyA

4

Код не мелкий.

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

Цитаты из проекта N4296:

1,9/15 Выполнение программы

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

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

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

Образец (G ++ 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, показывая (ноль) указатель перемещается в сторону.

+5

«Инициализация параметров функции» не является «выражением или выражением в теле вызываемой функции». Фактически, ваша собственная цитата говорит, что она «встречается в контексте вызывающей функции», а не вызываемой функции. –

2

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

Код не быстрее, если он находится на одной линии, и он часто является менее правильным.

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

Это как 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 проблемы с перемещением и ссылки на char).

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

+0

Первое решение кажется немного наивным для меня; Параметры rvalue-reference широко распространены с C++ 11, и как только вы подключаете более одной такой функции, передавая параметр через цепочку, вы рискуете получить вышеупомянутую ошибку, и она * отлично выглядит * переведите всю пересылку следующей функции в цепочке, включая перемещение. Второе решение приятное, но не просто доступное для всех, чтобы предотвратить их отключение от потенциальной ошибки. – Danra

+0

@ Danra Forwarding, не получая доступа к данным, так же безопасна, как и вызывающий: если они не отмечали мутацию одновременно с чтением, вы должны быть в порядке. Пересылка с дополнительным доступом к данным требует тщательного изучения того, что вещь, которую вы пересылаете, - это не то, что вы читаете в другом месте (поскольку пересылка - это возможный ход)? Можете ли вы построить явный пример счетчика? – Yakk

+0

Явный встречный пример к чему? Не уверен, что я получил. – Danra

1

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

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

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

+0

После того, как произойдет строительство перемещения, что-нибудь внутри p, будет ли POD или объект недопустимым, поэтому я не уверен, что получил суть вашего расширенного примера. Что касается философской части, я чувствую, что продемонстрированная ошибка может быть распространенной ошибкой, поэтому я надеялся, что есть более надежный способ ее избежать. Учитывая, что в настоящее время его нет, я надеюсь, что его можно добавить в std. – Danra

+0

@ Danra Дело в том, что даже если вы решили получить «p-> x' _prior_ для выдачи вызова, тогда, в зависимости от того, что' p-> x'_is_, вы все равно можете столкнуться с ошибками. Он просто показал, что есть много деталей, которые необходимо учитывать, чтобы написать правильный C++. Я бы сказал, что 'std :: move()' не является «всегда-всегда». Скорее, он должен использоваться, когда _necessary_ для достижения правильности (сначала), а затем производительности. Это неотъемлемо опасный инструмент, поскольку он позволяет вам сохранять доступ к недопустимым объектам. –

+0

Теперь я вижу вашу точку зрения. Но копирование/перемещение p-> x заблаговременно и все время жизни зависит от * p, звучит для меня так, как будто вы что-то неправильно сделали :) Однако, копирование p-> x в параметр вызова функции, в то же время когда вы перемещаете p в другой параметр вызова функции, кажется, что он должен работать - или, по крайней мере, выглядит так, как должен, в однострочном. – Danra

2

Как отмечено в Dieter Lücking's answer, значение вычисление секвенировало, прежде чем тело функции, так что std::move и operator -> секвенируют перед телом функции --- 1,9/15.

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

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

Насколько идиомы идти, чтобы избежать этого, рассмотреть David Haim's approach принимая unique_ptr<C> по ссылке, хотя это довольно непрозрачным для вызывающего абонента. Вы сигнализируете что-то вроде «Можете изменить этот указатель». unique_ptr Перемещение из состояния достаточно понятно, так что это вряд ли укусит вас так же плохо, как если бы вы перешли от переданной ссылки объекта или чего-то еще.

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

+1

На самом деле, P0145 исправит это, чтобы сделать это слева направо. –

+0

А, так оно и есть. Ответ обновлен. – TBBle