Как статическое утверждение в функции-члене только в том случае, если оно используется?

У меня есть следующая схема:

struct Baz {};
struct Qux {};

struct Base {
  virtual ~Base() {}
  virtual void foo() = 0;
};

template<typename T> struct Identity { static bool const value = false; };
template<typename T> void bar(T) { static_assert(Identity<T>::value, "Busted!!!"); }
template<> void bar<Baz>(Baz) {}

template<typename T>
struct Derived : Base {
  T m;
  void foo() { bar(m); }
};

int main() {
  Base *b0 = new Derived<Baz>;
  b0->foo();
  Base *b1 = new Derived<Qux>;
  (void) b1;
}

То есть, у меня есть чистый виртуальный класс Base и класс шаблона Derived, который наследует от Base и переопределяет чистую виртуальную функцию foo по мере необходимости. Теперь внутри foo я вызываю шаблон функции bar. bar имеет специализацию для класса Baz, но не для класса Qux. Когда в main, я пытаюсь материализовать объект Derived<Baz> все ОК. Но когда я пытаюсь материализовать объект Derived<Qux>, компилятор обращается к static_assert.

Q

Есть ли способ преобразовать мой код таким образом, чтобы компилятор попадал в static assert в Derived<Qux>, только если вызывается Derived<Qux>::foo().

То есть материализация объекта Derived<Qux> будет проходить:

Base *b1 = new Derived<Qux>;

Но когда позже в коде программист пытается вызвать:

b1->foo(); // compile error static assert

Ответ 1

В стандарте написано интересное: [temp.inst]/9:

Реализация не должна неявно создавать экземпляр функции шаблон, шаблон переменной, шаблон-член, не виртуальный член функция, класс-член, статический элемент данных шаблона класса или подстановка оператора constexpr if, если такое создание необходимо. Неизвестно, реализуется ли реализация неявно создает экземпляр виртуальной функции-члена шаблона класса если функция виртуального члена иначе не будет создана.

Решение о создании виртуальной функции зависит от реализации, но только если в противном случае это не требуется. Вопрос, с которым мы сталкиваемся, заключается в следующем: когда требуется определение, соответствующее самому стандарту?

Ответ на [class.virtual]/11 и [ temp.inst]/2:

Виртуальная функция, объявленная в классе, должна быть определена или объявлена ​​чистой в этом классе или и то, и другое; диагностика не требуется

Неявное инстанцирование специализации шаблона шаблона вызывает неявное создание объявлений, но не определений, аргументов по умолчанию или noexcept-спецификаций функций-членов класса

Таким образом, любая инстанция шаблона класса создаст декларацию Derived::foo, которая по цепной реакции требует определения. Таким образом, определение также должно создаваться, если оно доступно.

Единственный способ, которым реализация может реализовать свободу действий, указанную в первом цитируемом абзаце, заключается в том, что если Derived::foo также является чисто виртуальным. Например, оба Clang и GCC делают именно это. Это, конечно, вероятно, будет иметь ограниченную помощь вам.

Итак, чтобы сделать длинную историю коротким, это не-стартер, пока функция виртуальна (а не чистая виртуальная).

Ответ 2

@StoryTeller дает подробный ответ, ссылающийся на спецификацию и т.д., но я собираюсь ответить на ваш вопрос и спросить, что вы на самом деле пытаетесь сделать. По мере того, как вопрос написан, мертво очевидно, что ответ "нет", потому что вы запрашиваете ошибку времени компиляции на то, что определяется только во время выполнения. Например:.

Base *b;
if (user_input() == 42) {
     b = new Derived<Baz>();
} else {
     b = new Derived<Qux>();
}
b->foo();

Вы хотите ошибку компилятора для этого случая? Если это так, вам нужно определить условия, при которых, по вашему мнению, Qux::foo следует считать "вызванным". В настоящее время компилятор принимает метод, определяемый как виртуальный в экземплярированном классе. Ясно, что вы хотите что-то менее консервативное, но что?

Если у вас есть более конкретная информация о типе времени компиляции, может быть возможно уловить ошибку во время выполнения. Например.

Derived<Qux> d = new Derived<Qux>();
d->foo();

Если foo - не виртуальный шаблонный метод, возможно, он может проверить базовый тип во время компиляции и затем отправить виртуальный метод. (Вероятно, это потребует изменения подписи foo, чтобы иметь тип как-то.)

Лучшим решением было бы разбить различные типы функциональных возможностей интерфейса на разные классы и ввести механизм для получения конкретных интерфейсов для конкретного конкретного класса. Это можно проверить во время компиляции, если у вас есть конкретный тип класса в руке и во время выполнения, если вы выполняете динамический поиск интерфейса.