Функция проверки с заданной сигнатурой затем компилируется по-другому

#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. Это очень полный и простой в использовании. Большое спасибо за помощь!