Pull to refresh

Comments 19

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

Вообще говоря, можно. Даже стандартная библиотека, которая гарантирует "действительное, но не оговорённое" (valid but unspecified) состояние перемещённого объекта (раздел "Moved-from state of library types" [lib.types.movedfrom]), делает это исключительно по своей доброй воле.
Для обычного объекта вполне достаточно того, что перемещённый объект можно уничтожить (выполнить деструктор) или заново проинициализировать (выполнить любое присвоение).
Потому что в целом было бы странно продолжать нормальную работу с объектами, из которых вытащили все внутренности.

Немного уточню.


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

Спасибо, важное замечание! Добавлю в статью.

Что если я хочу написать так?


class T
{
public:
    T();
    T( const T& ) = delete;
};

T&& foo()
{
    return T();
}

T&& t = foo();

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


При этом я не хочу разрешать перемещать или копировать объект, или использовать placement new (не хочу руками вызывать деструктор).

На секунду появилась мысль написать так:


void bar(T& t)
{
    t.T::T();
}

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

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


Мы всё ещё делаем вид, что есть какое-то мнимое копирование, сдувая пыль со стандартов 20-летней давности.


Я даже не уверен, что если я напишу конструктор перемещения, то это не сделает ситуацию ещё хуже, вынудив компилятор отказаться от RVO. Это какой-то провал!

На всякий случай, чтобы не быть голословным. В main:


main:
        lea     rax, [rbp-17]  //выделили место на стеке
        mov     rdi, rax       //передаем адрес через rdi
        call    foo()

        lea     rax, [rbp-17]
        mov     rdi, rax
        call    T::~T()        //вызов деструктора для того же адреса

теперь смотрим foo:


foo:
        mov     QWORD PTR [rbp-8], rdi  //копирование туда-сюда
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    T::T()                  //вызов конструктора для адреса
                                        //в rdi в стеке main

Иногда действительно хочется отложенную инициализацию.

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

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

Однако, в C++17 все будет работать так, как вы хотите, и не иначе: возвращаемые значения будут сразу конструироваться на нужном месте (guaranteed copy elision).

Хороший комментарий. Вот, что интересно: если вы разбирались с этим — заданы ли четкие требования к работе copy elision? Например, в таком случае:


T foo()
{
    T t;
    if (rand() % 2)
    {
        T t;
        return t;
    }
    else
    {
        return t;
    }
}
Для возвращаемого значения copy elision делается для RVO
auto foo() -> T { return T{}; }


и для NRVO
auto foo() -> T { T t; return t; }


В вашем случае copy elision скорее всего сделан не будет.
UFO just landed and posted this here
Мне вот что интересно — если бы язык С++ можно было проектировать с нуля, не оглядываясь на «обратную совместимость», то как бы следовало спроектировать rvalue references? Так как сделано сейчас или как-то иначе?

Как раз недавно задумывался про это. В смысле, в расте всё удобно сделано, но задать своё "поведение перемещения" нельзя. Интересно можно ли придумать не сильно извращённую логику, когда этого в языке будет не хватать. Я так сходу не смог.

Мне нравится cpp тем, что в нем новые фичи реализованы средствами самого языка, как например, в тексте выше реализация std::move по сути каст к rvalue через иснтанцирование нужной специализации remove_reference. Но все это негативно сказывается на время компиляции… И вот скажите, как бы улучшилось время компиляции, если бы std::move был чем-то вроде зарезервированного компилятором имени, и преобразования делались на уровне парсера кода?
UFO just landed and posted this here
Так помимо приведения типа здесь также есть инстанцирование шаблона, что уже влияет существенно на время компиляции
UFO just landed and posted this here
Прям так сходу, с графиками и примерами не смогу. Но основываюсь на своем опыте и опыте коллег, которые жалуются, что с обильным использованием шаблонов в С++, время компиляции существенно вырастает. И сами посудите, вот такое неявное использование шаблонов в std::move при использовании его на большом кол-ве различных типов (а это в контексте использования std::move добиться, в приницпе, легко) генерит кучу дополнительного кода. На каждый уникальный тип объекта по инстансу remove_reference, когда могли бы просто получить подстановку нужного типа простой подменой на уровне парсинга кода.
Sign up to leave a comment.

Articles

Change theme settings