Должно ли NRVO гарантировать, что локальная именованная переменная и переменная call-site должны принимать тот же адрес?

Я думаю, это должно, потому что это важно для правильности. Тем не менее, я удивлен, увидев выход Clang. Рассмотрим следующий код:

#include <iostream>

struct S
{
    int i;

    S(int i) : i(i) {}

    S(S&&)
    {
        std::cout << "S(S&&)\n";
    }

    S(S const&) = delete;
};

S f()
{
    S s{42};
    std::cout << &s << "\n";
    return s;
}

int main()
{
    S s{f()};
    std::cout << &s << "\n";
    std::cout << s.i << "\n";
}

Мы определили перемещение ctor для S, чтобы проверить, вызывается ли S(S&&), если нет, применяется NRVO.

Результат из GCC:

0x7ffc3ed7b5ac
0x7ffc3ed7b5ac
42

применяется NRVO, и они принимают тот же адрес, который ожидается.

Однако, Clang output:

0x7fff908bbcc8
0x7fff908bbcf8
42

применяется NRVO, но адреса различаются.

В случае, если вы задаетесь вопросом, почему важно иметь тот же адрес, потому что какой-то объект может сделать некоторую регистрацию с его адресом при построении, и если объект перемещен, он должен быть уведомлен (например, через move-ctor).

Наличие NRVO, но с другим адресом памяти, тем самым делает его плохо сформированным. Это явное нарушение контракта - не вызывается пользовательский move/copy ctor, как компилятор "копирует" данные S в другое место?

Является ли это ошибкой в ​​Clang?


Если мы добавим деструктор к S, например

~S() {}

В этот раз, Clang выводит один и тот же адрес.

Ответ 1

Определенно кажется, что ошибка в clang, они должны быть одинаковыми, иначе такие вещи, как следующее, будут ошибочными

struct S
{
    int i;
    int* ptr;

    S(int i) : i(i) {
        this->ptr = &this->i;
    }

    S(S&& s)
    {
        this->i = s.i; 
        this->ptr = &this->i;
        std::cout << "S(S&&)\n";
    }

    S(S const&) = delete;
};

Если для обеспечения того, чтобы внутренний указатель указывал на правильное целое число, требуется переход (или исключение, где адреса не изменяются). Но из-за исключения, указатель указывает на память, которая не содержит целочисленное число.

Смотрите здесь здесь https://wandbox.org/permlink/NgNR0mupCfnnmlhK

Как указано в @T.C., это на самом деле ошибка в спецификации Itanium ABI, которая не учитывает move-ctor. Цитата из Clang dev:

Правило Клана - это одно из ABI: класс передается косвенно, если он имеет нетривиальный деструктор или нетривиальный конструктор копирования. Эта Правило определенно нуждается в некоторой корректировке [...]

В самом деле, если мы определяем либо нетривиальный dtor, либо copy-ctor для S в исходном примере, получаем ожидаемый результат (т.е. тот же адрес).