Встраивание приводит к тому, что специализированная функция-член класса, перекрывающая виртуальные функции, игнорируется

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

Чтобы этот пример работал, вам нужно:

  • треугольное виртуальное наследование (от функции-члена getAsString())
  • специализация функции-члена класса шаблона (здесь, Value<bool>::getAsString()) переопределение виртуальной функции
  • (автоматическое) встраивание компилятором

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

// test1.cpp and test2.cpp
#include <string>

class ValueInterface_common
{
public:
virtual ~ValueInterface_common() {}
virtual const std::string getAsString() const=0;
};

template <class T>
class Value :
virtual public ValueInterface_common
{
public:
virtual ~Value() {}
const std::string getAsString() const;
};

template <class T>
inline const std::string Value<T>::getAsString() const
{
return std::string("other type");
}

Далее мы должны унаследовать это Value класс и интерфейс в Parameter класс, который тоже должен быть шаблонным:

// test1.cpp
template <class T>
class Parameter :
virtual public Value<T>,
virtual public ValueInterface_common
{
public:
virtual ~Parameter() {}
const std::string getAsString() const;
};

template<typename T>
inline const std::string Parameter<T>::getAsString() const
{
return Value<T>::getAsString();
}

Теперь не (!) Не дайте предварительное объявление о специализации для Value для типа выравнивания bool …

// NOT in: test1.cpp
template <>
const std::string Value<bool>::getAsString() const;

Но вместо этого просто дайте его определение следующим образом …

// test2.cpp
template <>
const std::string Value<bool>::getAsString() const
{
return std::string("bool");
}

.. но в другом модуле (это важно)!

И наконец, у нас есть main() Функция для проверки происходящего:

// test1.cpp
#include <iostream>

int main(int argc, char **argv)
{
ValueInterface_common *paraminterface = new Parameter<bool>();
Parameter<int> paramint;
Value<int> valint;
Value<bool> valbool;
Parameter<bool> parambool;

std::cout << "paramint is " << paramint.getAsString() << std::endl;
std::cout << "parambool is " << parambool.getAsString() << std::endl;
std::cout << "valint is " << valint.getAsString() << std::endl;
std::cout << "valbool is " << valbool.getAsString() << std::endl;
std::cout << "parambool as PI is " << paraminterface->getAsString() << std::endl;

delete paraminterface;

return 0;
}

Если вы скомпилируете код следующим образом (я поместил его в два модуля с именами test1.cpp и test2.cpp, где последний содержит только специализацию и необходимые объявления):

g++ -O3 -g test1.cpp test2.cpp -o test && ./test

Выход

paramint is other type
parambool is other type
valint is other type
valbool is bool
parambool as PI is other type

Если вы компилируете с -O0 или просто -fno-inline — или также, если вы дадите предварительное объявление о специализации, — результатом станет:

paramint is other type
parambool is bool
valint is other type
valbool is bool
parambool as PI is bool

Забавно, не правда ли?

Мое объяснение до сих пор: Inlining работает в первом модуле (test.cpp). Необходимые шаблонные функции создаются, но некоторые просто оказываются встроенными в вызовы Parameter<bool>::getAsString(), С другой стороны, для valbool это не работает, но шаблон создается и используется как функция. Затем компоновщик находит как экземплярную шаблонную функцию, так и специализированную функцию, заданную во втором модуле, и принимает решение о последнем.

Что ты думаешь об этом?

  • Вы считаете это поведение ошибкой?
  • Почему встраивание работает для Parameter<bool>::getAsString() но не для Value<bool>::getAsString() хотя оба перекрывают виртуальную функцию?

4

Решение

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

По сути, Одно Правило Определения утверждает, что один и тот же объект должен
имеют одинаковое определение в приложении, в противном случае
эффекты не определено.

Основная проблема заключается в том, что код, который не видит специализированную версию вашей функции-члена шаблона класса, может все еще компилироваться, может ссылаться, а иногда даже запускаться. Это связано с тем, что при отсутствии (явной декларации) явной специализации включается неспециализированная версия, которая, вероятно, реализует универсальную функциональность, которая работает и для вашего специализированного типа.

Поэтому, если вам повезет, вы получите ошибку компилятора об отсутствующих объявлениях / определениях, но если вам действительно не повезет, вы получите «рабочий» код, который не соответствует тому, что вы намеревались сделать.

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

// my_template.hpp
#include "my_template_fwd.hpp"#include "my_template_primary.hpp"#include "my_template_spec_some_type.hpp"
// my_template_fwd.hpp
template<typename> class my_template; // forward declaration of the primary template

// my_template_primary.hpp
#include "my_template_fwd.hpp"template<typename T> class my_template { /* full definition */ };

// my_template_spec_some_type.hpp
#include "my_template_fwd.hpp"template<> class my_template<some_type> { /* full definition */ };

// some_client_module.hpp
#include "my_template.hpp" // no ODR possible, compiler will always see unique definition

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

4

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]