Я хотел поделиться странным примером с вами, ребята, на которого я наткнулся, и это заставило меня задуматься в течение двух дней.
Чтобы этот пример работал, вам нужно:
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()
хотя оба перекрывают виртуальную функцию?Я предполагаю, что у вас есть проблема 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
Очевидно, что вы могли бы реорганизовать именование, создав подкаталоги для специализаций шаблонов и изменив соответствующие пути включения.
Других решений пока нет …