#c #templates #macros
Вопрос:
В шаблонном классе мы можем проверить подпись функции-члена и определить различные способы компиляции для разных подклассов? Чтобы быть более конкретным, рассмотрим следующий простой пример:
template <typename T>
class Foo {
// ingore all other functions
virtual std::shared_ptr<T> do_something(int a) {
return std::make_shared<T>(a);
}
};
Это должно просто отлично работать с T1
классом:
class T1 {
T1(int a) {
// implememntation
}
// ingore all other functions
};
// Foo<T1> would just work
Однако это приведет к сбою компиляции, T2
Foo
потому что do_something
функция s явно реализована для вызова T
конструктора только с одним аргументом:
class T2 {
T2(int a, int b) {
// implememntation
}
// ingore all other functions
};
// Foo<T2> would fail to compile
Поэтому вопрос, мы можем доработать по Foo
, чтобы позволить ему работать для обоих T1
и T2
, таким образом, что, за T1
классы и чей конструктор принимает один int
аргумент, он будет компилироваться в реализацию по умолчанию, тогда как для T2
классов и чей конструкторов разных, он будет компилироваться с виртуальной функцией и реализации подкласса, чтобы реализовать его override
. Примерно так, как показано ниже:
Template <typename T>
class Foo {
// ingore all other functions
/* if T's signature takes one int input only
* compile like the below
*/
virtual std::shared_ptr<T> do_something(int x) {
return std::make_shared<T>(x);
}
/* if T's signature is different
* compile like the below and let the subclass implement it
*/
virtual std::shared_ptr<T> do_something(int x) = 0;
}
Возможно ли это без использования какой-либо сторонней библиотеки? Это приемлемо, если нам придется использовать макросы/препроцессоры.
Это также приемлемо, если там должна быть одна функция do_something
, но в случае несоответствующей подписи это просто вызывает исключение во время выполнения, что-то вроде:
Template <typename T>
class Foo {
// ingore all other functions
virtual std::shared_ptr<T> do_something(int x) {
/* if T's signature takes one int input only
* compile like the below
*/
// return std::make_shared<T>(x);
/* if T's signature is different
* compile like the below and let the subclass implement it
*/
// std::throws...
}
}
Комментарии:
1. Есть ли конкретная причина для
do_something
того, чтобы быть виртуальным?2. Да, потому что подклассы могут захотеть переопределить его в приложении; и другим функциям
Foo
, возможно, также потребуется вызватьdo_something
переопределенное.3. В C невозможно иметь два метода класса с одинаковой сигнатурой, и SFINAE не поможет. Что было бы возможно
Foo
, так это наследовать от специализированного класса, на основеT
специализации которого будет реализована соответствующаяdo_something
альтернатива. Это была бы куча кода, но это возможно, и то же самое нужно делать для каждойdo_something
перегрузки. Это самое большее, что можно здесь сделать.4. Почему бы просто не сделать
Foo::do_something()
шаблон переменной, чтобы он мог принимать любое количество параметров конструктора?5. Потому что это должно быть виртуальным, см. Выше.
Ответ №1:
Насколько я могу судить, здесь нам нужна специализация по шаблонам классов. Даже предложения C 20 requires
не могут быть применены к virtual
функциям, поэтому единственное, что мы можем сделать,- это изменить весь класс.
template<typename T> // using C 20 right now to avoid SFINAE madness
struct Foo {
virtual ~Foo() = defau<
virtual std::shared_ptr<T> do_something(int a) = 0;
};
template<std::constructible_from<int> T>
struct Foo<T> {
virtual ~Foo() = defau< // to demonstrate the issue of having to duplicate the rest of the class
virtual std::shared_ptr<T> do_something(int a) {
return std::make_shared<T>(a);
}
};
Если в нем много материала Foo
, вы можете избежать его дублирования с большими первоначальными затратами, перейдя do_something
в его собственный класс.
namespace detail { // this class should not be touched by users
template<typename T>
struct FooDoSomething {
virtual ~FooDoSomething() = defau<
virtual std::shared_ptr<T> do_something(int a) = 0;
};
template<std::constructible_from<int> T>
struct FooDoSomething<T> {
virtual ~FooDoSomething() = defau<
virtual std::shared_ptr<T> do_something(int a);
};
}
template<typename T>
struct Foo : detail::FooDoSomething<T> {
// other stuff, not duplicated
// just an example
virtual int foo(int a) = 0;
};
namespace detail {
template<std::constructible_from<int> T>
std::shared_ptr<T> FooDoSomething<T>::do_something(int a) {
Foo<T> amp;thiz = *static_cast<Foo<T>*>(this); // if you need "Foo things" in this default implementation, then FooDoSomething is *definitely* unsafe to expose to users!
return std::make_shared<T>(thiz.foo(a));
}
}
Чтобы преобразовать это из C 20, замените специализацию, основанную на концепции, ветвлением старого типа:
// e.g. for the simple solution
template<typename T, bool = std::is_constructible_v<T, int>>
struct Foo { // false case
// etc.
};
template<typename T>
struct Foo<T, true> { // true case
// etc.
};
// or do this to FooDoSomething if you choose to use that
Ошибка во время выполнения намного проще, по крайней мере, в C 17 и выше, так как вы можете просто использовать a if constexpr
, чтобы избежать компиляции проблемного кода
template<typename T>
struct Foo {
virtual ~Foo() = defau<
virtual std::shared_ptr<T> do_something(int a) {
if constexpr(std::is_constructible_v<T, int>) return std::make_shared<T>(a);
else throw std::logic_error("unimplemented Foo<T>::do_something");
}
};
Комментарии:
1. Это очень полный и простой в использовании. Большое спасибо за помощь!