Как стать автором
Обновить

Комментарии 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 скорее всего сделан не будет.
НЛО прилетело и опубликовало эту надпись здесь
Мне вот что интересно — если бы язык С++ можно было проектировать с нуля, не оглядываясь на «обратную совместимость», то как бы следовало спроектировать rvalue references? Так как сделано сейчас или как-то иначе?

Вероятно, как в Rust.

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

Мне нравится cpp тем, что в нем новые фичи реализованы средствами самого языка, как например, в тексте выше реализация std::move по сути каст к rvalue через иснтанцирование нужной специализации remove_reference. Но все это негативно сказывается на время компиляции… И вот скажите, как бы улучшилось время компиляции, если бы std::move был чем-то вроде зарезервированного компилятором имени, и преобразования делались на уровне парсера кода?
НЛО прилетело и опубликовало эту надпись здесь
Так помимо приведения типа здесь также есть инстанцирование шаблона, что уже влияет существенно на время компиляции
НЛО прилетело и опубликовало эту надпись здесь
Прям так сходу, с графиками и примерами не смогу. Но основываюсь на своем опыте и опыте коллег, которые жалуются, что с обильным использованием шаблонов в С++, время компиляции существенно вырастает. И сами посудите, вот такое неявное использование шаблонов в std::move при использовании его на большом кол-ве различных типов (а это в контексте использования std::move добиться, в приницпе, легко) генерит кучу дополнительного кода. На каждый уникальный тип объекта по инстансу remove_reference, когда могли бы просто получить подстановку нужного типа простой подменой на уровне парсинга кода.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории