Преобразование из базы данных в неполные типы, необходимые для decltype

Я наткнулся на этот фрагмент кода, включающий конечные типы возврата и наследование.

Следующий минимальный пример прекрасно компилируется с g++, а не с clang

struct Base {};

int foo(Base&) {
    return 42;
}

struct Derived : public Base {
    auto bar() -> decltype(foo(*this)) {
        return foo(*this);
    }
};

int main()
{
    Derived derived;
    derived.bar();  

    return 0;
}

Однако если мы изменим auto bar() → decltype(foo(*this)) на decltype(auto) bar() (расширение С++ 14), код также будет компилироваться с помощью clang. Ссылка на Godbolt https://godbolt.org/z/qf_k6X.

Может кто-нибудь объяснить мне

  • чем auto bar() → decltype(return expression) отличается от decltype(auto) bar()
  • почему поведение между компиляторами отличается
  • что такое правильная реализация?

Ответы

Ответ 1

Это ошибка gcc, конечный тип возврата не находится в контексте полного класса [class.mem]

Полный класс класса является

  • функциональное тело,
  • аргумент по умолчанию,
  • noexcept-спецификатор ([кроме .spec]),
  • условие договора или
  • инициализатор элемента по умолчанию

Мы видим, что для преобразования из производного в базовый из [conv.ptr] необходим полный класс

Значение типа "указатель на cv D", где D - полный тип класса, может быть преобразовано в значение типа "указатель на cv B", где B - базовый класс D.

и [dcl.init.ref]

"cv1 T1" совместим со ссылками с "cv2 T2", если значение типа "указатель на cv2 T2" можно преобразовать в тип "указатель на cv1 T1" через стандартную последовательность преобразования. Во всех случаях, когда эталонно-совместимые отношения двух типов используются для установления достоверности эталонной привязки, а стандартная последовательность преобразования будет некорректной, программа, для которой требуется такая привязка, некорректна.

С другой стороны, тело функции находится в контексте полного класса, и, следовательно, преобразование из производной в базовую форму является корректным. Тип возврата, включающий тип заполнителя (decltype(auto)) , действителен, если он уже выведен до выражения, использующего его.

Для возможного обходного пути в С++ 11 вы можете использовать

auto bar() -> decltype(foo(std::declval<Base&>()))
{
    return foo(*this);
}

при условии, что вы знаете, что хотите позвонить с помощью Base.

Ответ 2

Я думаю, что Clang не прав, чтобы отклонить это:

Что касается типа возврата определения функции, стандарт С++ 14 гласит:

[dcl.fct]/9]

Типы не должны быть определены в возвращаемых или типах параметров. Тип параметра или возвращаемого типа для определения функции не должен быть неполным типом класса (возможно, cv-квалифицированным), если только функция не удалена (8.4.3) или определение не вложено в спецификацию члена для этого класса ( включая определения во вложенных классах, определенных в классе).

В вашем примере определение bar вложено в спецификацию члена class Derived. Так что это разрешено, и GCC, ICC и MSVC получают это право.

С другой стороны, decltype(auto) работает, потому что выведенный тип возврата фактически не выводится до тех пор, пока не понадобится подпись функции. И в вашем случае это происходит, когда вы вызываете bar() в main. В этот момент class Derived является полностью определенным типом. Clang понимает это правильно.

Обратите внимание, что даже использование auto вместо decltype(auto) будет работать для вашего примера. Посмотрите Демо на Godbolt.

Ответ 3

Derived является неполным типом до }; достигается, поэтому Clang правильно отклоняет этот код, потому что неполный тип не может быть неявно преобразован в ссылку на полный тип. Чтобы сделать этот код действительным, вы можете использовать decltype(auto) или просто auto в качестве возвращаемого типа. Это немного отличается, хотя. Это похоже на ошибку GCC. Я верю, что языковой адвокат скоро приедет и процитирует соответствующие разделы стандарта!