Может ли шаблон функции-члена класса C++ быть виртуальным?

Я слышал, что шаблоны функций-членов класса C++ не могут быть виртуальными. Это правда?

Если они могут быть виртуальными, то каков пример сценария, в котором можно использовать такую ​​функцию?

15 ответов

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

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

Тем не менее, есть несколько мощных и интересных методов, вытекающих из сочетания полиморфизма и шаблонов, особенно так называемое стирание типов.

Из шаблонов C++ Полное руководство:

Шаблоны функций-членов не могут быть объявлены виртуальными. Это ограничение наложено, потому что обычная реализация механизма вызова виртуальной функции использует таблицу фиксированного размера с одной записью на виртуальную функцию. Однако количество экземпляров шаблона функции-члена не фиксируется до тех пор, пока не будет переведена вся программа. Следовательно, поддержка шаблонов виртуальных функций-членов потребует поддержки совершенно нового механизма в компиляторах и компоновщиках C++. Напротив, обычные члены шаблонов классов могут быть виртуальными, потому что их число фиксируется при создании экземпляра класса.

C++ не разрешает виртуальные функции-члены шаблона прямо сейчас. Наиболее вероятной причиной является сложность его реализации. Раджендра приводит веские причины, почему это нельзя сделать прямо сейчас, но это возможно при разумных изменениях стандарта. Особенно трудно определить, сколько экземпляров шаблонной функции действительно существует, и создание виртуальной таблицы кажется трудным, если учесть место вызова виртуальной функции. У людей, работающих над стандартами, сейчас есть много других дел, и C++1x - это тоже большая работа для авторов компиляторов.

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

Таблицы виртуальных функций

Давайте начнем с некоторой предыстории таблиц виртуальных функций и того, как они работают ( источник):

[20.3] В чем разница между вызовом виртуальных и не виртуальных функций-членов?

Не виртуальные функции-члены разрешаются статически. То есть функция-член выбирается статически (во время компиляции) в зависимости от типа указателя (или ссылки) на объект.

Напротив, виртуальные функции-члены разрешаются динамически (во время выполнения). То есть функция-член выбирается динамически (во время выполнения) в зависимости от типа объекта, а не от типа указателя / ссылки на этот объект. Это называется "динамическое связывание". Большинство компиляторов используют какой-либо вариант следующего метода: если объект имеет одну или несколько виртуальных функций, компилятор помещает в объект скрытый указатель, называемый "виртуальный указатель" или "v-указатель". Этот v-указатель указывает на глобальную таблицу, называемую "виртуальная таблица" или "v-таблица".

Компилятор создает v-таблицу для каждого класса, который имеет хотя бы одну виртуальную функцию. Например, если у класса Circle есть виртуальные функции для draw() и move() и resize(), то будет точно одна v-таблица, связанная с классом Circle, даже если бы существовал gazillion объектов Circle, и указатель v каждый из этих объектов Circle будет указывать на v-таблицу Circle. Сама v-таблица имеет указатели на каждую из виртуальных функций в классе. Например, v-таблица Circle будет иметь три указателя: указатель на Circle::draw(), указатель на Circle::move() и указатель на Circle::resize().

Во время отправки виртуальной функции система во время выполнения следует за v-указателем объекта на v-таблицу класса, а затем за соответствующим слотом в v-таблице к коду метода.

Затраты на место в указанной выше методике являются номинальными: дополнительный указатель на объект (но только для объектов, которым потребуется динамическое связывание), плюс дополнительный указатель на метод (но только для виртуальных методов). Затраты времени и средств также довольно номинальны: по сравнению с обычным вызовом функции, вызов виртуальной функции требует двух дополнительных выборок (одна для получения значения v-указателя, вторая для получения адреса метода). Ни одно из этих действий во время выполнения не происходит с не виртуальными функциями, поскольку компилятор разрешает не виртуальные функции исключительно во время компиляции на основе типа указателя.


Моя проблема или как я сюда попал

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

Некоторый код:

virtual void  LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

Что бы я хотел, но оно не скомпилируется из-за виртуальной шаблонной комбинации:

template<class T>
    virtual void  LoadCube(UtpBipCube<T> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

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

Решение

предупреждение, это не очень красиво, но это позволило мне удалить повторяющийся код выполнения

1) в базовом классе

virtual void  LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

2) и в детских классах

void  LoadCube(UtpBipCube<float> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

template<class T>
void  LoadAnyCube(UtpBipCube<T> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1);

Обратите внимание, что LoadAnyCube не объявлен в базовом классе.


Вот еще один ответ о переполнении стека с обходным решением: нужен обходной путь для члена виртуального шаблона.

Нет, они не могут. Но:

template<typename T>
class Foo {
public:
  template<typename P>
  void f(const P& p) {
    ((T*)this)->f<P>(p);
  }
};

class Bar : public Foo<Bar> {
public:
  template<typename P>
  void f(const P& p) {
    std::cout << p << std::endl;
  }
};

int main() {
  Bar bar;

  Bar *pbar = &bar;
  pbar -> f(1);

  Foo<Bar> *pfoo = &bar;
  pfoo -> f(1);
};

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

Следующий код можно скомпилировать и запустить правильно, используя MinGW G++ 3.4.5 в Windows 7:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class A{
public:
    virtual void func1(const T& p)
    {
        cout<<"A:"<<p<<endl;
    }
};

template <typename T>
class B
: public A<T>
{
public:
    virtual void func1(const T& p)
    {
        cout<<"A<--B:"<<p<<endl;
    }
};

int main(int argc, char** argv)
{
    A<string> a;
    B<int> b;
    B<string> c;

    A<string>* p = &a;
    p->func1("A<string> a");
    p = dynamic_cast<A<string>*>(&c);
    p->func1("B<string> c");
    B<int>* q = &b;
    q->func1(3);
}

и вывод:

A:A<string> a
A<--B:B<string> c
A<--B:3

И позже я добавил новый класс X:

class X
{
public:
    template <typename T>
    virtual void func2(const T& p)
    {
        cout<<"C:"<<p<<endl;
    }
};

Когда я попытался использовать класс X в main() следующим образом:

X x;
x.func2<string>("X x");

g ++ сообщает о следующей ошибке:

vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'

Итак, очевидно, что:

  • Виртуальная функция-член может использоваться в шаблоне класса. Компилятору легко построить vtable
  • Невозможно определить функцию-член шаблона класса как виртуальную, как вы можете видеть, сложно определить сигнатуру функции и выделить записи vtable.

Нет, функции-члены шаблона не могут быть виртуальными.

В других ответах предложенная функция шаблона представляет собой фасад и не дает никакой практической выгоды.

  • Шаблонные функции полезны для написания кода только один раз, используя разные типы.
  • Виртуальные функции полезны для того, чтобы иметь общий интерфейс для разных классов.

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

Однако для каждой комбинации типов шаблонов необходимо определить фиктивную функцию виртуальной оболочки:

#include <memory>
#include <iostream>
#include <iomanip>

//---------------------------------------------
// Abstract class with virtual functions
class Geometry {
public:
    virtual void getArea(float &area) = 0;
    virtual void getArea(long double &area) = 0;
};

//---------------------------------------------
// Square
class Square : public Geometry {
public:
    float size {1};

    // virtual wrapper functions call template function for square
    virtual void getArea(float &area) { getAreaT(area); }
    virtual void getArea(long double &area) { getAreaT(area); }

private:
    // Template function for squares
    template <typename T>
    void getAreaT(T &area) {
        area = static_cast<T>(size * size);
    }
};

//---------------------------------------------
// Circle
class Circle : public Geometry  {
public:
    float radius {1};

    // virtual wrapper functions call template function for circle
    virtual void getArea(float &area) { getAreaT(area); }
    virtual void getArea(long double &area) { getAreaT(area); }

private:
    // Template function for Circles
    template <typename T>
    void getAreaT(T &area) {
        area = static_cast<T>(radius * radius * 3.1415926535897932385L);
    }
};


//---------------------------------------------
// Main
int main()
{
    // get area of square using template based function T=float
    std::unique_ptr<Geometry> geometry = std::make_unique<Square>();
    float areaSquare;
    geometry->getArea(areaSquare);

    // get area of circle using template based function T=long double
    geometry = std::make_unique<Circle>();
    long double areaCircle;
    geometry->getArea(areaCircle);

    std::cout << std::setprecision(20) << "Square area is " << areaSquare << ", Circle area is " << areaCircle << std::endl;
    return 0;
}

Выход:

Площадь квадрата 1, площадь круга 3.1415926535897932385

Попробуй здесь

Чтобы ответить на вторую часть вопроса:

Если они могут быть виртуальными, то каков пример сценария, в котором можно использовать такую ​​функцию?

Это не необоснованная вещь, чтобы хотеть сделать. Например, Java (где каждый метод является виртуальным) не имеет проблем с общими методами.

Одним из примеров C++, когда требуется шаблон виртуальной функции, является функция-член, которая принимает универсальный итератор. Или функция-член, которая принимает объект универсальной функции.

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

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

// abstract.h

// Simply define the types that each concrete class will use
#define IMPL_RENDER() \
    void render(int a, char *b) override { render_internal<char>(a, b); }   \
    void render(int a, short *b) override { render_internal<short>(a, b); } \
    // ...

class Renderable
{
public:
    // Then, once for each on the abstract
    virtual void render(int a, char *a) = 0;
    virtual void render(int a, short *b) = 0;
    // ...
};

Итак, теперь, чтобы реализовать наш подкласс:

class Box : public Renderable
{
public:
    IMPL_RENDER() // Builds the functions we want

private:
    template<typename T>
    void render_internal(int a, T *b); // One spot for our logic
};

Преимущество здесь в том, что при добавлении нового поддерживаемого типа все это можно сделать из абстрактного заголовка и отказаться от возможного исправления его в нескольких файлах источника / заголовка.

Существует обходной путь для "метода виртуального шаблона", если набор типов для метода шаблона известен заранее.

Чтобы показать идею, в приведенном ниже примере используются только два типа (int а также double).

Там, "виртуальный" метод шаблона (Base::Method) вызывает соответствующий виртуальный метод (один из Base::VMethod) который, в свою очередь, вызывает реализацию метода шаблона (Impl::TMethod).

Нужно только реализовать метод шаблона TMethod в производных реализациях (AImpl, BImpl) и использовать Derived<*Impl>,

class Base
{
public:
    virtual ~Base()
    {
    }

    template <typename T>
    T Method(T t)
    {
        return VMethod(t);
    }

private:
    virtual int VMethod(int t) = 0;
    virtual double VMethod(double t) = 0;
};

template <class Impl>
class Derived : public Impl
{
public:
    template <class... TArgs>
    Derived(TArgs&&... args)
        : Impl(std::forward<TArgs>(args)...)
    {
    }

private:
    int VMethod(int t) final
    {
        return Impl::TMethod(t);
    }

    double VMethod(double t) final
    {
        return Impl::TMethod(t);
    }
};

class AImpl : public Base
{
protected:
    AImpl(int p)
        : i(p)
    {
    }

    template <typename T>
    T TMethod(T t)
    {
        return t - i;
    }

private:
    int i;
};

using A = Derived<AImpl>;

class BImpl : public Base
{
protected:
    BImpl(int p)
        : i(p)
    {
    }

    template <typename T>
    T TMethod(T t)
    {
        return t + i;
    }

private:
    int i;
};

using B = Derived<BImpl>;

int main(int argc, const char* argv[])
{
    A a(1);
    B b(1);
    Base* base = nullptr;

    base = &a;
    std::cout << base->Method(1) << std::endl;
    std::cout << base->Method(2.0) << std::endl;

    base = &b;
    std::cout << base->Method(1) << std::endl;
    std::cout << base->Method(2.0) << std::endl;
}

Выход:

0
1
2
3

NB:Base::Method на самом деле излишек для реального кода (VMethod могут быть обнародованы и использованы напрямую). Я добавил его так, чтобы он выглядел как настоящий "виртуальный" метод шаблона.

По крайней мере, в gcc 5.4 виртуальные функции могут быть членами шаблона, но должны быть самими шаблонами.

#include <iostream>
#include <string>
class first {
protected:
    virtual std::string  a1() { return "a1"; }
    virtual std::string  mixt() { return a1(); }
};

class last {
protected:
    virtual std::string a2() { return "a2"; }
};

template<class T>  class mix: first , T {
    public:
    virtual std::string mixt() override;
};

template<class T> std::string mix<T>::mixt() {
   return a1()+" before "+T::a2();
}

class mix2: public mix<last>  {
    virtual std::string a1() override { return "mix"; }
};

int main() {
    std::cout << mix2().mixt();
    return 0;
}

Выходы

mix before a2
Process finished with exit code 0

Мое текущее решение следующее (с отключенным RTTI - вы также можете использовать std::type_index):

#include <type_traits>
#include <iostream>
#include <tuple>

class Type
{
};

template<typename T>
class TypeImpl : public Type
{

};

template<typename T>
inline Type* typeOf() {
    static Type* typePtr = new TypeImpl<T>();
    return typePtr;
}

/* ------------- */

template<
    typename Calling
    , typename Result = void
    , typename From
    , typename Action
>
inline Result DoComplexDispatch(From* from, Action&& action);

template<typename Cls>
class ChildClasses
{
public:
    using type = std::tuple<>;
};

template<typename... Childs>
class ChildClassesHelper
{
public:
    using type = std::tuple<Childs...>;
};

//--------------------------

class A;
class B;
class C;
class D;

template<>
class ChildClasses<A> : public ChildClassesHelper<B, C, D> {};

template<>
class ChildClasses<B> : public ChildClassesHelper<C, D> {};

template<>
class ChildClasses<C> : public ChildClassesHelper<D> {};

//-------------------------------------------

class A
{
public:
    virtual Type* GetType()
    {
        return typeOf<A>();
    }

    template<
        typename T,
        bool checkType = true
    >
        /*virtual*/void DoVirtualGeneric()
    {
        if constexpr (checkType)
        {
            return DoComplexDispatch<A>(this, [&](auto* other) -> decltype(auto)
                {
                    return other->template DoVirtualGeneric<T, false>();
                });
        }
        std::cout << "A";
    }
};

class B : public A
{
public:
    virtual Type* GetType()
    {
        return typeOf<B>();
    }
    template<
        typename T,
        bool checkType = true
    >
    /*virtual*/void DoVirtualGeneric() /*override*/
    {
        if constexpr (checkType)
        {
            return DoComplexDispatch<B>(this, [&](auto* other) -> decltype(auto)
                {
                    other->template DoVirtualGeneric<T, false>();
                });
        }
        std::cout << "B";
    }
};

class C : public B
{
public:
    virtual Type* GetType() {
        return typeOf<C>();
    }

    template<
        typename T,
        bool checkType = true
    >
    /*virtual*/void DoVirtualGeneric() /*override*/
    {
        if constexpr (checkType)
        {
            return DoComplexDispatch<C>(this, [&](auto* other) -> decltype(auto)
                {
                    other->template DoVirtualGeneric<T, false>();
                });
        }
        std::cout << "C";
    }
};

class D : public C
{
public:
    virtual Type* GetType() {
        return typeOf<D>();
    }
};

int main()
{
    A* a = new A();
    a->DoVirtualGeneric<int>();
}

// --------------------------

template<typename Tuple>
class RestTuple {};

template<
    template<typename...> typename Tuple,
    typename First,
    typename... Rest
>
class RestTuple<Tuple<First, Rest...>> {
public:
    using type = Tuple<Rest...>;
};

// -------------
template<
    typename CandidatesTuple
    , typename Result
    , typename From
    , typename Action
>
inline constexpr Result DoComplexDispatchInternal(From* from, Action&& action, Type* fromType)
{
    using FirstCandidate = std::tuple_element_t<0, CandidatesTuple>;

    if constexpr (std::tuple_size_v<CandidatesTuple> == 1)
    {
        return action(static_cast<FirstCandidate*>(from));
    }
    else {
        if (fromType == typeOf<FirstCandidate>())
        {
            return action(static_cast<FirstCandidate*>(from));
        }
        else {
            return DoComplexDispatchInternal<typename RestTuple<CandidatesTuple>::type, Result>(
                from, action, fromType
            );
        }
    }
}

template<
    typename Calling
    , typename Result
    , typename From
    , typename Action
>
inline Result DoComplexDispatch(From* from, Action&& action)
{
    using ChildsOfCalling = typename ChildClasses<Calling>::type;
    if constexpr (std::tuple_size_v<ChildsOfCalling> == 0)
    {
        return action(static_cast<Calling*>(from));
    }
    else {
        auto fromType = from->GetType();
        using Candidates = decltype(std::tuple_cat(std::declval<std::tuple<Calling>>(), std::declval<ChildsOfCalling>()));
        return DoComplexDispatchInternal<Candidates, Result>(
            from, std::forward<Action>(action), fromType
        );
    }
}

Единственное, что мне не нравится, это то, что вам нужно определять / регистрировать все дочерние классы.

Я просмотрел все 14 ответов. У некоторых есть причины, по которым функции виртуальных шаблонов не могут работать, другие показывают обходной путь. Один ответ даже показал, что виртуальные классы могут иметь виртуальные функции. Что не должно слишком удивлять.

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

Во всяком случае. Я уверен, что стандарт требует, чтобы указатель this на объект был первым аргументом его функции-члена.

      struct MyClass
{
 void myFunction();
}

// translate to
void myFunction(MyClass*);

Теперь, когда мы ясно об этом. Затем нам нужно знать правила преобразования для шаблонов. Шаблонный параметр чрезвычайно ограничен тем, во что он может неявно преобразовываться. всего не помню, но можно посмотреть для полной справки. Например, T* можно преобразовать в const T*. Массивы можно преобразовать в указатели. Однако производный класс нельзя преобразовать в базовый класс в качестве шаблонного параметра.

      struct A {};
struct B : A {};

template<class T>
void myFunction(T&);

template<>
void myFunction<A>(A&) {}

int main()
{
 A a;
 B b;

 myFunction(a); //compiles perfectly
 myFunction((A&)b); // compiles nicely
 myFunction(b); //compiler error, use of undefined template function
}

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

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

Как вызывается правая функция в случае виртуальной?

Vtable будет содержать записи для каждой виртуальной функции класса, и во время выполнения он выберет адрес конкретной функции и вызовет соответствующую функцию.

Как правильно вызывать функцию в случае виртуальной вместе с шаблоном функции?

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

Другие вопросы по тегам