Могу ли я гарантировать RVO для повторно преобразованных значений?

Предположим, я написал:

Foo get_a_foo() {
    return reinterpret_cast<Foo>(get_a_bar());
}

и предположим, что sizeof(Foo) == sizeof(Bar).

Обязательно ли здесь имеет место оптимизация возвращаемого значения, или компиляторы могут делать все, что им заблагорассудится, когда я «нарушаю правила» с помощью reinterpret_cast? Если я не получу RVO или мне это не гарантируется, могу ли я изменить этот код, чтобы гарантировать, что это произойдет?

Мой вопрос касается С++ 11 и отдельно С++ 17 (поскольку в нем были некоторые изменения относительно RVO, если я не ошибаюсь).


person einpoklum    schedule 05.01.2018    source источник
comment
@FrançoisAndrieux, этого не может быть, иначе это не скомпилировалось бы.   -  person Quentin    schedule 05.01.2018
comment
или компиляторам разрешено делать все, что им заблагорассудится, когда я «нарушаю правила»? Да, в общем, компилятор может делать все, что хочет, когда у вас неопределенное поведение. В этом случае, если Foo не является указателем, ссылкой, целым числом или типом перечисления, результатом, скорее всего, будет ошибка компиляции. Если это один из них, то вам, вероятно, не нужен RVO, потому что возвращаемое значение тривиально копируемое и маленькое, и в большинстве архитектур оно будет помещено в регистр.   -  person Daniel H    schedule 05.01.2018
comment
Если у вас есть конкретный пример, вы можете узнать, что фактические компиляторы будут делать на реальном оборудовании. Затем либо надейтесь, что поведение будет стабильным на протяжении всей жизни вашей программы, и вы не пропустили комбинацию при тестировании, либо ищите флаги, которые влияют на то, какое поведение не определено (например, различные флаги для отключения строгого правила псевдонимов) и используйте эти . Если вы выберете этот последний вариант, вам нужно прочитать руководства по компилятору для ответов, и вы больше не говорите о стандартном С++.   -  person Daniel H    schedule 05.01.2018
comment
Насколько я понимаю, реализациям компилятора на С++ 11 разрешено выполнять RVO, но ничто не гарантирует этого.   -  person the4thamigo_uk    schedule 05.01.2018
comment
@the4thamigo_uk: Да, до C++17 это было разрешено, но не гарантировано . Гарантии C++17 означают, что вы можете писать соответствующий стандартам код, в котором тип не имеет ни конструктора перемещения, ни конструктора копирования, но все же может быть возвращен из функции по значению; в C++11 это было несовместимо, потому что компиляторам без RVO разрешалось перемещать или копировать его, в C++17 они должны RVO. К сожалению, гарантия не распространяется на NRVO, потому что правила для NRVO более сложны, поэтому на практике вам, вероятно, не следует отключать копирование/перемещение для шуток. :-)   -  person ShadowRanger    schedule 05.01.2018


Ответы (1)


Предположим, я написал:

Foo get_a_foo() {
    return reinterpret_cast<Foo>(get_a_bar());
}

и предположим, что sizeof(Foo) == sizeof(Bar).

Это reinterpret_cast недопустимо для всех возможных типов Foo и Bar. Это работает только в случаях, когда:

  1. Bar — это указатель, а Foo — либо указатель, либо целое число/перечисление, достаточно большое для хранения указателей.
  2. Bar — целое число/перечисление, достаточно большое для хранения указателя, а Foo — указатель.
  3. Bar — это тип объекта, а Foo — это ссылочный тип.

Есть пара других случаев, которые я не освещал, но они либо не имеют отношения к делу (nullptr_t кастинг), либо подпадают под аналогичные проблемы для № 1 или № 2.

Видите ли, элизия на самом деле не имеет значения при работе с фундаментальными типами. Вы не можете сказать разницу между исключением копирования/перемещения фундаментальных типов и его отсутствием. Так есть ли там конверсия? Компилятор просто использует регистр возвращаемого значения? Это зависит от компилятора с помощью правила «как если бы».

И elision не применяется при возврате ссылочных типов, поэтому № 3 отсутствует.

Но если Foo и Bar являются определяемыми пользователем типами объектов (или типами объектов, отличными от указателей, целых чисел или указателей-членов), приведение будет имеет неправильный формат. reinterpret_cast не является какой-то тривиальной функцией преобразования memcpy.

Итак, давайте заменим это некоторым кодом, который действительно может работать:

Foo get_a_foo()
{
    return std::bit_cast<Foo>(get_a_bar());
}

Где C++20 std::bit_cast эффективно преобразует один тривиальный копируемый тип в другой тривиальный копируемый тип.

Это преобразование все еще не было бы исключено. Или, по крайней мере, не в том смысле, в каком обычно используется слово «элизион».

Поскольку два типа легко копируются, а bit_cast будет вызывать только тривиальные конструкторы, компилятор, безусловно, может стереть конструкторы и даже использовать объект возвращаемого значения get_a_foo в качестве объекта возвращаемого значения get_a_bar. Таким образом, это можно было бы считать «элизионом».

Но «выброс» обычно относится к той части стандарта, которая позволяет реализации игнорировать даже нетривиальные конструкторы/деструкторы. Компилятор может выполнять только описанное выше, потому что все конструкторы и деструкторы тривиальны. Если бы они были нетривиальными, их нельзя было бы игнорировать (опять же, если бы они были нетривиальными, std::bit_cast не работало бы).

Я хочу сказать, что оптимизация приведенного выше преобразования не связана с правилами «элизион» или RVO; это полностью связано с правилом «как если бы». Даже в C++17 вопрос о том, будет ли вызов bit_cast эффективным оператором noop, полностью зависит от компилятора. Да, после создания значения Foo prvalue "выведение" его копии в объект возвращаемого значения функции требуется для С++ 17.

Но само обращение не является вопросом исключения.

person Nicol Bolas    schedule 05.01.2018