Защо преместването на променлива на указател не го настройва на нула?

Когато се прилагат конструктори за преместване и оператори за присвояване на преместване, често се пише код по следния начин:

p = other.p;
other.p = 0;

Неявно дефинираните операции за преместване ще бъдат реализирани с код като този:

p = std::move(other.p);

Което би било погрешно, защото преместването на променлива на указател не я настройва на нула. Защо така? Има ли случаи, в които бихме искали операциите за преместване да оставят оригиналната променлива на указателя непроменена?

Забележка: Под „преместване“ не имам предвид само подизраза std::move(other.p), имам предвид целия израз p = std::move(other.p). И така, защо няма специално езиково правило, което да гласи „Ако дясната страна на дадено присвояване е указател xvalue, той се настройва на нула, след като присвояването е извършено.“?


person fredoverflow    schedule 26.02.2012    source източник
comment
Защо трябва да има? Единственото нещо, което трябва да направите с "преместен" обект, е да го игнорирате. Наличието на указател, сочещ към памет, която не е собственост на класа, не би трябвало да е проблем, ако вече не използвате указателя, нали?   -  person    schedule 26.02.2012
comment
Не плащате за това, което не използвате?   -  person Bo Persson    schedule 26.02.2012
comment
@hvd: Деструкторът със сигурност няма да го игнорира, ако пише delete p :)   -  person fredoverflow    schedule 26.02.2012
comment
@FredOverflow Деструкторът със сигурност ще го игнорира, ако не казва delete p -- откъде компилаторът знае? Ако изисквате указателят да бъде зададен на null, задайте го на null, не карайте компилатора да го прави за класове, които не се нуждаят от него.   -  person    schedule 26.02.2012
comment
@hvd: Ако сами напишете деструктор, никакви операции за преместване няма да бъдат автоматично генерирани от компилатора и тогава няма какво да обсъждаме.   -  person fredoverflow    schedule 26.02.2012
comment
@FredOverflow И автоматично генериран деструктор никога няма да каже delete p, така че какъв е смисълът, който се опитвате да направите?   -  person    schedule 26.02.2012
comment
@FredOverflow: delete p е лош аргумент, необработените указатели никога не трябва да притежават.   -  person Xeo    schedule 26.02.2012
comment
@hvd: Бих могъл просто да кажа T& operator=(T&&) = default; и с моето предложение това ще направи правилното нещо.   -  person fredoverflow    schedule 26.02.2012
comment
@Xeo добра точка. Преместването на интелигентни указатели върши правилното нещо, така че ако само интелигентните указатели притежават нещо, тогава преместването на необработени указатели вече върши правилното нещо. Ако имате указател, който изтривате ръчно, обвийте го в unique_ptr   -  person bames53    schedule 26.02.2012
comment
std::swap върху два указателя е отличен пример за това, къде не искате std::move да автоматично NULL указатели. И какво е нулевото състояние за цели числа? Вярно е, че един оптимизатор би могъл да реши случая std::swap да бъде отново идеален, но мисля, че такива случаи показват, че е по-добре да го оставим на мира.   -  person Johannes Schaub - litb    schedule 26.02.2012
comment
Опаковането на вашите указатели в клас обвивка, чийто ctor за преместване и присвояване op автоматично нулира указателя, ще ви позволи да използвате неявния ctor/присвояване за преместване за съдържащи класове.   -  person Johannes Schaub - litb    schedule 26.02.2012
comment
@JohannesSchaub-litb Бихте ли разяснили по-подробно проблема с std::swap? Не съм сигурен какво имаш предвид. Неефективност поради ненужно нулиране?   -  person fredoverflow    schedule 26.02.2012
comment
Note: By "moving", I do not just mean the subexpression std::move(other.p) Ето защо std::move е нелепо @!#^ име за тази конструкция.   -  person Lightness Races in Orbit    schedule 26.02.2012
comment
@Lightness Straustrup казва: move(x) означава, че можете да лекувате x като rстойност. Може би щеше да е по-добре, ако move() се наричаше rval(), но досега move() се използва от години.   -  person fredoverflow    schedule 26.02.2012
comment
@FredOverflow: Да, това би било много по-добро име за него. Или get_rvalue(). Вместо това това е удобно за потребителя име, избрано поради предполагаемия най-често използван случай, но тъй като това се отнася само във връзка с други езикови функции (т.е. действително се движи), мисля, че издава целия принцип на C++. Бих приел аргументацията, че е твърде късно за промяна (и под това имам предвид по време на късните етапи на стандартизацията на C++11, когато разбрах за това бедствие).   -  person Lightness Races in Orbit    schedule 26.02.2012
comment
@FredOverflow Ако предложа компилаторът винаги да извиква delete на всички членове на указателя в автоматично генерирания деструктор, мога просто да кажа ~T() = default; и това ще направи правилното нещо, нали? Ама добре... ;)   -  person Christian Rau    schedule 26.02.2012
comment
дори rval би било неправилно, защото rval(function) дава lvalue.   -  person Johannes Schaub - litb    schedule 26.02.2012
comment
@BoPersson Вие изрично го използвате с операция за преместване някъде във веригата, така че мисля, че плащането за това би било добре. Копирането ще запази поведението си по подразбиране и по-евтино.   -  person ryancerium    schedule 29.08.2016


Отговори (5)


Задаването на необработен указател на нула след преместването му предполага, че указателят представлява собственост. Въпреки това, много указатели се използват за представяне на връзки. Освен това, дълго време се препоръчва отношенията на собственост да се представят по различен начин от използването на необработен указател. Например отношението на собственост, за което говорите, е представено от std::unique_ptr<T>. Ако искате неявно генерираните операции за преместване да се грижат за вашата собственост, всичко, което трябва да направите, е да използвате членове, които всъщност представляват (и прилагат) желаното поведение на собственост.

Освен това поведението на генерираните операции за преместване е в съответствие с това, което е направено с операциите за копиране: те също не правят никакви допускания за собственост и не правят напр. дълбоко копие, ако е копиран указател. Ако искате това да се случи, трябва също да създадете подходящ клас, кодиращ съответната семантика.

person Dietmar Kühl    schedule 26.02.2012
comment
Току-що си спомних, че прочетох статия, в която се казваше, че преместването никога не трябва да е по-скъпо от копирането или нещо подобно. Задаването на оригиналния указател null би нарушило това правило. Знаете ли за коя хартия говоря или мозъкът ми го е измислил? - person fredoverflow; 26.02.2012
comment
Не съм напълно добре с първото изречение. Задаването на необработен указател на нула след преместването му предполага, че указателят представлява собственост. Защо така? Не съм съгласен. В една съвременна база кодова купчина собствеността се управлява от интелигентни указатели. Необработените указатели са просто еднакви към препратка, с изключение на това, че разработчикът има опцията да не препраща към нищо. В стара кодова база не знаете дали указателят се използва за притежаване на купчина или просто за препратка към нещо. По този начин задаването на указател на null IMHO не означава, че го притежавате. - person Don Pedro; 03.07.2019

Преместването прави преместения обект "невалиден". Той не го задава автоматично в безопасно „празно“ състояние. В съответствие с дългогодишния принцип на C++ "не плащайте за това, което не използвате", това е ваша работа, ако я искате.

person Lightness Races in Orbit    schedule 26.02.2012

Мисля, че отговорът е: прилагането на такова поведение сами е доста тривиално и следователно Стандартът не чувства нужда да налага каквото и да е правило на самия компилатор. Езикът C++ е огромен и не всичко може да се измисли преди използването му. Вземете например шаблона на C++. Първоначално не е проектиран да се използва по начина, по който се използва днес (т.е. това е способност за метапрограмиране). Така че мисля, че Стандартът просто дава свободата и не е направил никакво конкретно правило за std::move(other.p), следвайки един от него е принципът на дизайна: „Не плащате за това, което не използвате“.

Въпреки че std::unique_ptr може да се премества, но не може да се копира. Така че, ако искате семантика на указател, която може да се движи и копира едновременно, тогава ето една тривиална реализация:

template<typename T>
struct movable_ptr
{
    T *pointer;
    movable_ptr(T *ptr=0) : pointer(ptr) {} 
    movable_ptr<T>& operator=(T *ptr) { pointer = ptr; return *this; }
    movable_ptr(movable_ptr<T> && other) 
    {
        pointer = other.pointer;
        other.pointer = 0;
    }
    movable_ptr<T>& operator=(movable_ptr<T> && other) 
    {
        pointer = other.pointer;
        other.pointer = 0;
        return *this;
    } 
    T* operator->() const { return pointer; }
    T& operator*() const { return *pointer; }

    movable_ptr(movable_ptr<T> const & other) = default;
    movable_ptr<T> & operator=(movable_ptr<T> const & other) = default;
};

Сега можете да пишете класове, без да пишете своя собствена семантика на движение:

struct T
{
   movable_ptr<A> aptr;
   movable_ptr<B> bptr;
   //...

   //and now you could simply say
   T(T&&) = default; 
   T& operator=(T&&) = default; 
};

Обърнете внимание, че все още трябва да напишете семантика за копиране и деструктор, тъй като movable_ptr не е интелигентен указател.

person Nawaz    schedule 26.02.2012
comment
@Fred - Би било, ако използвате достатъчно интелигентен указател, вместо да съхранявате необработени указатели. Възможно ли е това да е проблемът? - person Bo Persson; 26.02.2012
comment
@BoPersson: Мисля, че това трябва да е отговор? - person Nawaz; 26.02.2012
comment
@BoPersson Това и Вие не плащате за това, което не използвате, би било отличен отговор. - person fredoverflow; 26.02.2012
comment
@FredOverflow: Неявно дефинираните оператори за преместване извикват ли std::move? - person Nawaz; 26.02.2012
comment
Операциите за преместване са просто дефинирани за извикване на операциите за преместване на членовете, вижте 12.8 §15. - person fredoverflow; 26.02.2012
comment
@FredOverflow: В такъв случай това грешка в компилатора ли е: ideone.com/tMqUo .. Или пропускам нещо? (Очаквах moved (using move-assignment) като изход). - person Nawaz; 26.02.2012
comment
12.8 §9 казва, че вашият очакван резултат трябва да бъде правилният. ideone използва gcc-4.5.1, най-доброто ми предположение е, че тази версия все още не изпълнява правилата правилно. - person fredoverflow; 26.02.2012
comment
@FredOverflow: Ааа.. Пробвах го на моя локален компютър и даде очаквания. Както и да е, актуализирах отговора си с моите експерименти. - person Nawaz; 26.02.2012

Например, ако имате указател към споделен обект. Не забравяйте, че след преместване обектът трябва да остане във вътрешно последователно състояние, така че задаването на указател, който не трябва да е null, към null стойност, не е правилно.

I.e.:

struct foo
{
  bar*  shared_factory;  // This can be the same for several 'foo's
                         // and must never null.
};

Редактиране

Ето един цитат за MoveConstructibe от стандарта:

T u = rv;
...
rv’s state is unspecified [ Note:rv must still meet the requirements
of the library component that is using it. The operations listed in
those requirements must work as specified whether rv has been moved
from or not.
person doublep    schedule 26.02.2012
comment
Но настройването на променливата на указателя на null ще се случи само на xvalues. Клиентът няма да може да провери нулевия указател след това. - person fredoverflow; 26.02.2012
comment
Ами ако вашият деструктор, например, използва указателя по някакъв начин и ще извика UB, ако е нула? Също така, не мога да подкрепя твърдението си с цитат от стандарта, но доколкото си спомням, след преместване обектът трябва да остане валиден (в това, че можете да продължите да го използвате нормално), а не само разрушим. - person doublep; 26.02.2012
comment
@FredOverflow: намери подходящ цитат в стандарта. - person doublep; 26.02.2012
comment
@doublep: Изискването MoveConstructible е само за стандартните контейнери, нищо друго. Освен това вие дефинирате вътрешното съгласувано състояние на вашите обекти. - person Xeo; 26.02.2012
comment
@Xeo: Дори и да е така, би било доста странно, ако C++ stdlib съдържа функции (нулиране на преместени указатели), които противоречат на собствените му изисквания за други части. - person doublep; 26.02.2012
comment
@Xeo: Освен това трябва да следвате MoveConstructible изискванията, за да сте сигурни, че вашите обекти работят добре със стандартни контейнери, които изискват елементите да бъдат MoveConstructible, например. - person doublep; 26.02.2012
comment
Можете ли да обясните защо не можем да променим вашето изявление на ... така че преместването от указател, който не трябва да е нула, не е правилно.? - person Johannes Schaub - litb; 26.02.2012
comment
@Johannes Schaub: Конструкторът за преместване по подразбиране ще премести всички полета, без да обръща внимание дали е правилен или не. Също така, в метапрограмирането на шаблони е полезно да се третират всички типове еднакво и да се приеме, че ако даден тип има конструктор за преместване, той е преместваем (в противен случай преместването имплицитно ще се върне към копиране). - person doublep; 26.02.2012

Мисля, че това, което прави разликата тук, е напълно издухан обект от една страна и POD от друга страна.

  • За обекти или имплементаторът определя какво трябва да прави конструкцията на преместване и присвояването на преместване, или компилаторът генерира стойност по подразбиране. По подразбиране се извикват конструкторите за преместване/операторите за присвояване на всички членове.
  • За POD (а указателят е POD) C++ наследява от C и нищо не се прави с него, когато не е изрично кодиран. Това е просто същото поведение, както членовете на POD в клас се третират в конструктор. Ако не ги поставите изрично в списъка на инициализаторите, тогава те остават неинициализирани и са източник на потенциални грешки. AFAIK това е валидно дори за генерирани от компилатор конструктори! Ето защо възприех навика като цяло да инициализирам всички членове, за да съм в безопасност.
person Don Pedro    schedule 03.07.2019