Подтвердить что ты не робот

Зависимый тип или аргумент в decltype в определении функции не компилируется при объявлении без объявления

Я играл с выведенными типами возвращаемого значения в определениях, которые разрешают тот же тип, что и объявление. Это работает:

template <typename>
struct Cls {
  static std::size_t f();
};

template <typename T>
decltype(sizeof(int)) Cls<T>::f()  { return 0; }

Но если я изменю определение на что-то, что должно быть эквивалентно, заменив sizeof(int) на sizeof(T) он не работает

template <typename T>
decltype(sizeof(T)) Cls<T>::f() { return 0; }

gcc error (clang почти идентичен):

error: prototype for ‘decltype (sizeof (T)) Cls<T>::f()’ does not match any in class ‘Cls<T>’
 decltype(sizeof(T)) Cls<T>::f() { return 0; }
                     ^~~~~~
so.cpp:4:24: error: candidate is: static std::size_t Cls<T>::f()
     static std::size_t f();
                        ^

Та же проблема возникает при использовании типов параметров функции:

template <typename>
struct Cls {
  static void f(std::size_t);
};

template <typename T>
void Cls<T>::f(decltype(sizeof(T))) { } // sizeof(int) works instead

Еще страннее, если соответствие декларации и определения и оба используют decltype(sizeof(T)), он успешно компилируется, и я могу static_assert, что тип возврата size_t. Следующие компиляции успешно выполняются:

#include <type_traits>

template <typename T>
struct Cls {
  static decltype(sizeof(T)) f();
};

template <typename T>
decltype(sizeof(T)) Cls<T>::f() { return 0; }

static_assert(std::is_same<std::size_t, decltype(Cls<int>::f())>{}, "");

Обновление с помощью другого примера. Это не зависимый тип, но все еще не выполняется.

template <int I>
struct Cls {
  static int f();
};

template <int I>
decltype(I) Cls<I>::f() { return I; }

Если я использую decltype(I) как в определении, так и в декларации, он работает, если я использую int как в определении, так и в декларации, но он имеет два отличия.


Обновление 2: Аналогичный пример. Если Cls изменен, чтобы не быть шаблоном класса, он успешно компилируется.

template <typename>
struct Cls {
  static int f();
  using Integer = decltype(Cls::f());
};

template <typename T>
typename Cls<T>::Integer Cls<T>::f() { return I; }

Обновление 3: Другой неудачный пример от М.М. Шаблонная функция-член неимметрированного класса.

struct S {
  template <int N>
  int f();
};

template <int N>
decltype(N) S::f() {}

Почему это незаконно, если декларация и определение не согласуются только с зависимым типом? Почему это затрагивается, даже если сам тип не зависит от template <int I> выше?

4b9b3361

Ответ 1

Поскольку при наличии параметра шаблона decltype возвращает тип зависимости, зависящий от типа, в соответствии со стандартом, см. ниже. Если параметр шаблона отсутствует, он разрешает очевидный size_t. Таким образом, в этом случае вы должны выбрать либо объявление, либо определение имеют независимое выражение (например, size_t/decltype(sizeof(int))), как возвращаемый тип, или оба имеют зависимое выражение (например, decltype(sizeof(T))), которое разрешено к уникальному зависимому типу и рассматривается эквивалентны, если их выражения эквивалентны (см. ниже).

В этом сообщении я использую стандартный проект С++ N3337.

§ 7.1.6.2 [dcl.type.simpl]

¶ 4

Тип, обозначенный как decltype (e), определяется следующим образом: - если e является несферированным id-выражением или несвязанным доступом к члену класса (5.2.5), decltype (e) является типом объекта, названного e. Если такой объект отсутствует или если e называет набор перегруженных функций, программа плохо организована,

- в противном случае, если e является значением x, decltype (e) является T & &, где T - тип e;

- в противном случае, если e является lvalue, decltype (e) является T &, где T - тип e;

- в противном случае decltype (e) - тип e.

Это объясняет, что такое decltype(sizeof(int)). Но для decltype(sizeof(T)) есть другой раздел, объясняющий, что это такое.

§ 14.4 [temp.type]

¶ 2

Если выражение e включает параметр шаблона, decltype (e) обозначает уникальный зависимый тип. Два таких Спецификаторы decltype относятся к одному типу, только если их выражения эквивалентны (14.5.6.1). [Примечание: однако, он может быть псевдонимом, например, с помощью typedef-name. - конечная нота]

В источниках Clang LLVM версии 3.9 в файле lib/AST/Type.cpp

DecltypeType::DecltypeType(Expr *E, QualType underlyingType, QualType can)
  // C++11 [temp.type]p2: "If an expression e involves a template parameter,
  // decltype(e) denotes a unique dependent type." Hence a decltype type is
  // type-dependent even if its expression is only instantiation-dependent.
  : Type(Decltype, can, E->isInstantiationDependent(),
         E->isInstantiationDependent(),
         E->getType()->isVariablyModifiedType(),
         E->containsUnexpandedParameterPack()),

Важная фраза начинается с "Следовательно, decltype...". Он снова разъясняет ситуацию.

Снова в источниках Clang версии 3.9 в файле lib/AST/ASTContext.cpp

QualType ASTContext::getDecltypeType(Expr *e, QualType UnderlyingType) const {
  DecltypeType *dt;

  // C++11 [temp.type]p2:
  //   If an expression e involves a template parameter, decltype(e) denotes a
  //   unique dependent type. Two such decltype-specifiers refer to the same
  //   type only if their expressions are equivalent (14.5.6.1).
  if (e->isInstantiationDependent()) {
    llvm::FoldingSetNodeID ID;
    DependentDecltypeType::Profile(ID, *this, e);

    void *InsertPos = nullptr;
    DependentDecltypeType *Canon
      = DependentDecltypeTypes.FindNodeOrInsertPos(ID, InsertPos);
    if (!Canon) {
      // Build a new, canonical typeof(expr) type.
      Canon = new (*this, TypeAlignment) DependentDecltypeType(*this, e);
      DependentDecltypeTypes.InsertNode(Canon, InsertPos);
    }
    dt = new (*this, TypeAlignment)
        DecltypeType(e, UnderlyingType, QualType((DecltypeType *)Canon, 0));
  } else {
    dt = new (*this, TypeAlignment)
        DecltypeType(e, UnderlyingType, getCanonicalType(UnderlyingType));
  }
  Types.push_back(dt);
  return QualType(dt, 0);
}

Итак, вы видите, что Clang собирает и выбирает эти уникальные зависимые типы decltype в/из специального набора.

Почему компилятор настолько глуп, что не видит, что выражение decltype равно sizeof(T), которое всегда size_t? Да, это очевидно для человека-читателя. Но когда вы разрабатываете и реализуете формальные грамматические и семантические правила, особенно для таких сложных языков, как С++, вам приходится группировать проблемы и определять правила для них, а не просто придумывать правило для каждой конкретной проблемы, в последнем вы просто не сможете переместиться с вашим языком/компилятором. То же самое здесь нет справедливого правила: если decltype имеет выражение вызова функции, которое не требует разрешения параметров шаблона - разрешите decltype на возвращаемый тип функции. Существует более того, есть так много случаев, которые вам нужно уделить, что вы придумали более общее правило, например, приведенное выше из стандарта (14.4[2]).

Кроме того, аналогичный неочевидный случай с auto, decltype(auto), найденный AndyG в С++ - 14 (N4296, § 7.1.6.4 [dcl.spec.auto] 12/13):

§ 7.1.6.4 [dcl.spec.auto]

¶ 13

Редекларации или специализации шаблона функции или функции с объявленным типом возврата, который использует тип заполнителя также должен использовать этот заполнитель, а не выведенный тип. [Пример:

auto f();
auto f() { return 42; } // return type is int
auto f();               // OK
int f();                // error, cannot be overloaded with auto f()
decltype(auto) f();     // error, auto and decltype(auto) don’t match

Изменения в С++ 17, номер документa >= N4582

Изменение стандартного черновика N4582 с марта 2016 года (благодаря bogdan) обобщает утверждение:

§ 17.4 (старый § 14.4) [temp.type]

¶ 2

Если выражение e зависит от типа (17.6.2.2), decltype (e) обозначает уникальный зависимый тип. Два таких Спецификаторы decltype относятся к одному типу, только если их выражения эквивалентны (17.5.6.1). [Примечание: однако, такой тип может быть псевдонимом, например, typedef-name. - конечная нота]

Это изменение приводит к другому разделу, описывающему зависимое от типа выражение, которое выглядит довольно странным для нашего конкретного случая.

§ 17.6.2.2 [temp.dep.expr] (старый § 14.6.2.2)

¶ 4

Выражения следующих форм никогда не зависят от типа (поскольку тип выражения не может быть зависимый):

...
sizeof ( type-id )
...

Существуют дополнительные разделы зависимых от стоимости выражений, где sizeof может быть зависящим от значения, если зависимо от type-id. Связь между зависимым от стоимости выражением и decltype отсутствует. После некоторого раздумья я не нашел причин, почему decltype(sizeof(T)) не должен или не может разрешить в size_t. И я бы предположил, что это было довольно подлое изменение ( "включает параметр шаблона" в "зависящий от типа" ) в стандарте, который разработчики компилятора не обращали особого внимания (возможно, перегружены многими другими изменениями, возможно, не думали, что это может на самом деле что-то изменить, просто упростить формулировку). Изменение имеет смысл, поскольку sizeof(T) не зависит от типа, он зависит от стоимости. Оператор decltype(e) - неоцениваемый операнд, т.е. не заботится о значении, только о типе. Поэтому decltype возвращает уникальный тип только тогда, когда e зависит от типа. sizeof(e) может быть только зависимым от значения.

Я пробовал код с clang 5, gcc 8 -std=c++1z - тот же результат: ошибка. Я пошел дальше и пробовал этот код:

template <typename>
struct Cls {
  static std::size_t f();
};

template <typename T>
decltype(sizeof(sizeof(T))) Cls<T>::f() {
  return 0;
}

Произошла та же ошибка, даже если sizeof(sizeof(T)) не зависит от типа или значения (см. этот пост). Это дает мне основание предполагать, что компиляторы работают по-старому стандарту С++ - 11/14 (т.е. "Включает параметр шаблона" ), как в исходном фрагменте выше, из источника clang 3.9 (я могу проверить, что последняя разработка clang 5.0 имеет одинаковые строки, не нашел ничего связанного с этим новым изменением в стандарте), но не зависит от типа.