Пространство имен вызывает неоптимальное разрешение перегрузки шаблона

Это очень похоже на этот вопрос, но я не уверен, что ответ полностью применим к минимальному коду, который я ve собрал, что демонстрирует проблему. (В моем коде не используются возвращаемые типы, а также есть некоторые другие отличия.) Кроме того, проблема законности поведения MSVC, по-видимому, не решена.

Короче говоря, я вижу, что компилятор выбирает экземпляр общего шаблона функции, а не более конкретную перегрузку, когда шаблон функции находится внутри пространства имен.

Рассмотрим следующий набор определений пространств имен и классов:

namespace DoStuffUtilNamespace
{
  template<typename UNKNOWN>
  void doStuff(UNKNOWN& foo)
  {
    static_assert(sizeof(UNKNOWN) == -1, "CANNOT USE DEFAULT INSTANTIATION!");
  }
}

class UtilForDoingStuff
{
  public:
    template <typename UNKNOWN>
      void doStuffWithObjectRef(UNKNOWN& ref)
      {
        DoStuffUtilNamespace::doStuff(ref);
      }
};

class MyClassThatCanDoStuff { };

namespace DoStuffUtilNamespace
{
  using ::MyClassThatCanDoStuff;      // No effect.
  
  void doStuff(MyClassThatCanDoStuff& foo) { /* No assertion! */ }
}

... и следующие варианты использования:

int main()
{
  MyClassThatCanDoStuff foo;
  DoStuffUtilNamespace::MyClassThatCanDoStuff scoped_foo;
  UtilForDoingStuff util;
  
  DoStuffUtilNamespace::doStuff(foo);         // Compiles
  DoStuffUtilNamespace::doStuff(scoped_foo);  // Compiles
  util.doStuffWithObjectRef(foo);             // Triggers static assert
  util.doStuffWithObjectRef(scoped_foo);      // Triggers static assert
}

Если весь DoStuffUtilNamespace удаляется, а все его элементы перемещаются в глобальную область, это прекрасно компилируется с G++ и Clang++.

С пространством имен doStuff, конечно же, является зависимым именем. Согласно ответу с наибольшим количеством голосов на аналогичный вопрос, стандарт гласит:

При разрешении зависимых имен учитываются имена из следующих источников:

  • Объявления, видимые в точке определения шаблона.

  • Объявления из пространств имен, связанных с типами аргументов функции, как из контекста инстанцирования, так и из контекста определения.

Мне это кажется немного странным; Я не понимаю, почему в первом пункте указывается, что объявления должны быть видны в точке определения шаблона, а не в точке создания экземпляра, поскольку второй пункт явно указывает, что разрешены некоторые объявления, видимые только в точке инстанцирования, разрешены. (Если кто-то захочет предложить обоснование, я был бы признателен, но это не мой вопрос, потому что я понимаю, что вопросы в форме, почему комитет по стандартам решил X, не по теме.)

Итак, я думаю, это объясняет, почему util.doStuffWithObjectRef(foo); запускает статическое утверждение: doStuff(MyClassThatCanDoStuff&) не было объявлено в точке определения UtilForDoingStuff::doStuffWithObjectRef<UNKNOWN>(UNKNOWN&). И действительно, перемещение определения class UtilForDoingStuff после определения перегрузки doStuff, кажется, решает проблему.

Но что именно подразумевает стандарт под пространствами имен, связанными с типами аргументов функции? Не должно ли объявление using ::MyClassThatCanDoStuff вместе с явным определением области действия типа экземпляра scoped_foo в пространстве имен инициировать поиск, зависящий от аргумента, и не должен ли этот поиск найти неутверждающее определение doStuff()?

Кроме того, весь код компилируется без ошибок с использованием clang++ -ftemplate-delayed-parsing, который имитирует поведение MSVC при синтаксическом анализе шаблонов. Это кажется предпочтительным, по крайней мере, в данном конкретном случае, потому что возможность добавлять новые объявления в пространство имен в любое время является одним из основных преимуществ пространств имен. Но, как отмечалось выше, согласно стандарту, это не совсем соответствует букве закона. Допустимо ли это или это случай несоответствия?

EDIT:: Как указал KIIV, есть обходной путь; код компилируется, если вместо перегрузки используется специализация шаблона. Хотелось бы еще узнать ответы на свои вопросы по стандарту.


person Kyle Strand    schedule 26.08.2015    source источник


Ответы (3)


С пространством имен doStuff, конечно, является зависимым именем.

Вы исходите из неверной предпосылки. Для квалифицированного вызова, такого как DoStuffUtilNamespace::doStuff(ref), ADL не существует. [basic.lookup.argdep]/p1, выделение мое:

Когда постфиксное-выражение в вызове функции (5.2.2) является неполным идентификатором, другие пространства имен не учитываются при обычном неквалифицированном поиске. (3.4.1) могут быть найдены, и в этих пространствах имен могут быть найдены дружественные функции пространства имен или объявления шаблонов функций (11.3), которые иначе не видны.

DoStuffUtilNamespace::doStuff — это полный идентификатор, а не неполный идентификатор. АДЛ не применяется.

По этой причине DoStuffUtilNamespace::doStuff также не является зависимым именем. [temp.dep]/p1:

В выражении вида:

постфиксное-выражение ( список-выраженийopt)

где постфиксное-выражение — это неполный идентификатор, неполный идентификатор обозначает зависимое имя, если [. ..]. Если операнд оператора является выражением, зависящим от типа, оператор также обозначает зависимое имя. Такие имена не привязаны и просматриваются в точке создания экземпляра шаблона (14.6.4.1) как в контексте определения шаблона, так и в контексте точки создания экземпляра.

(Выделение зависимого имени курсивом указывает на то, что этот абзац определяет термин.)

Вместо этого для [temp.nondep]/p1:

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

который не находит ваше более позднее объявление перегрузки.


Специализация работает, потому что используется все то же объявление шаблона функции; вы только что предоставили другую реализацию, чем по умолчанию.


Но что именно подразумевает стандарт под «пространствами имен, связанными с типами аргументов функции»? Не должно ли объявление using ::MyClassThatCanDoStuff вместе с явным определением области действия типа экземпляра scoped_foo в пространстве имен запускать поиск, зависящий от аргумента?

Нет. декларации использования не влияют на ADL. [basic.lookup.argdep]/p2, выделение мое:

Для каждого типа аргумента T в вызове функции существует набор из нуля или более связанных пространств имен и набор из нуля или более связанных классов, которые необходимо учитывать. Наборы пространств имен и классов полностью определяются типами аргументов функции (и пространством имен любого аргумента шаблона шаблона). Имена Typedef и объявления использования, используемые для указания типов, не входят в этот набор. Наборы пространств имен и классов определяются следующим образом:

  • Если T фундаментальный тип, [...]

  • Если T является типом класса (включая объединения), его ассоциированными классами являются: сам класс; класс, членом которого он является, если таковой имеется; и его прямые и косвенные базовые классы. Связанные с ним пространства имен являются самыми внутренними объемлющими пространствами имен связанных с ним классов. Кроме того, если T является специализацией шаблона класса, связанные с ним пространства имен и классы также включают: пространства имен и классы, связанные с типами аргументов шаблона, предоставляемых для параметров типа шаблона (исключая параметры шаблона шаблона); пространства имен, членами которых являются любые аргументы шаблона шаблона; и классы, членами которых являются любые шаблоны-члены, используемые в качестве аргументов шаблона шаблона. [ Примечание: Аргументы шаблона, не относящиеся к типу, не влияют на набор связанных пространств имен. —конец примечания ]

  • [...]

person T.C.    schedule 16.09.2015

Со специализацией шаблона я могу заставить его работать:

namespace DoStuffUtilNamespace
{
  template<typename UNKNOWN>
  void doStuff(UNKNOWN& foo)
  {
    static_assert(sizeof(UNKNOWN) == -1, "CANNOT USE DEFAULT INSTANTIATION!");
  }
}

class UtilForDoingStuff
{
  public:
    template <typename UNKNOWN>
      void doStuffWithObjectRef(UNKNOWN& ref)
      {
        DoStuffUtilNamespace::doStuff(ref);
      }
};

class MyClassThatCanDoStuff { };


namespace DoStuffUtilNamespace
{
  using ::MyClassThatCanDoStuff;
  template <> void doStuff<MyClassThatCanDoStuff>(MyClassThatCanDoStuff& foo) { /* No assertion! */ }
}


int main()
{
  MyClassThatCanDoStuff foo;
  DoStuffUtilNamespace::MyClassThatCanDoStuff scoped_foo; 
  UtilForDoingStuff util;

  DoStuffUtilNamespace::doStuff(foo);         // Compiles
  DoStuffUtilNamespace::doStuff(scoped_foo);  // Compiles
  util.doStuffWithObjectRef(foo);             // Compiles
  util.doStuffWithObjectRef(scoped_foo);      // Compiles
}
person KIIV    schedule 27.08.2015
comment
....Хм. Да, похоже, это работает (хотя специализация шаблона должна быть объявлена ​​inline в нетривиальном случае с несколькими файлами). Есть идеи, почему версия с перегрузкой не работает, а версия со специализацией работает? - person Kyle Strand; 27.08.2015

Объявления из пространств имен, связанных с типами аргументов функции, как из контекста инстанцирования, так и из контекста определения.

Пример со следующим кодом, который печатает B::foo Demo

namespace A
{
    template <typename T>
    void foo(const T&) {std::cout << "A::foo" << std::endl;}

    template <typename T>
    void bar(const T& t) {
        foo(t); // thank to ADL, it will  also look at B::foo for B::S.
    }
}

namespace B
{
    struct S {};

    void foo(const S&) {std::cout << "B::foo" << std::endl;}
}

int main()
{
    B::S s;
    A::bar(s);
}

Таким образом, при вызове ?::foo(const B::S&) второй пункт списка добавляет B::foo в список перегрузок.

почему специализация шаблона работает в этом случае

Есть только одна функция:

template<>
void DoStuffUtilNamespace::doStuff<MyClassThatCanDoStuff>(MyClassThatCanDoStuff& foo);

даже если это будет определено позже. Обратите внимание, что в единице перевода должно быть известно о наличии специализации, иначе программа будет плохо сформирована (не соблюдает ODR).

при перегрузке нет.

Думаешь:

Поэтому я думаю, что это объясняет, почему util.doStuffWithObjectRef(foo); запускает статическое утверждение: doStuff(MyClassThatCanDoStuff&) не было объявлено в точке определения UtilForDoingStuff::doStuffWithObjectRef<UNKNOWN>(UNKNOWN&). И действительно, перемещение определения класса UtilForDoingStuff после того, как была определена перегрузка doStuff, кажется, решает проблему.

Точно.

person Jarod42    schedule 15.09.2015