Подтвердить что ты не робот

Альтернатива статическим виртуальным методам С++

В С++ невозможно объявить статическую виртуальную функцию, не прикладывать нестационарную функцию к указателю функции стиля C.

Теперь у меня есть простой OL C SDK, который сильно использует указатели на функции.

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

Также эта сигнатура функции C SDK не имеет параметра userData.

Есть ли хорошая альтернатива? Лучше всего я могу описать некоторые чистые виртуальные методы GetFuncA(), GetFuncB(),... и некоторые статические члены FuncA()/FuncB() в каждом производном классе, которые будут возвращены GetFuncX(). Тогда функция в абстрактном классе вызовет эти функции, чтобы получить указатели и заполнить структуру.

Edit Отвечая на вопрос Джона Диблинга, было бы здорово это сделать:

class Base
{
    FillPointers() { myStruct.funA = myFunA; myStruct.funB = myFunB; ...}
private:
    CStruct myStruct;
    static virtual myFunA(...) = 0;
    static virtual myFunB(...) = 0;
};

class Derived1 : public Base
{
    Derived1() {  FillPointers();  }
    static virtual myFunA(...) {...};
    static virtual myFunB(...) {...};
};

class Derived2 : public Base
{
    Derived2() {  FillPointers();  }
    static virtual myFunA(...) {...};
    static virtual myFunB(...) {...};
};

int main()
{
    Derived1 d1;
    Derived2 d2;
    // Now I have two objects with different functionality
}
4b9b3361

Ответ 1

Вы можете сделать Base шаблон класса, который принимает указатели на его аргументы шаблона:

extern "C" {
struct CStruct
{
  void (*funA)(int, char const*);
  int (*funB)(void);
};
}

template <typename T>
class Base
{
public:
  CStruct myStruct;
  void FillPointers() {
    myStruct.funA = &T::myFunA;
    myStruct.funB = &T::myFunB;
  }
  Base() {
    FillPointers();
  }
};

Затем определите производные классы для спуска из экземпляра Base с использованием каждого производного класса в качестве аргумента шаблона:

class Derived1: public Base<Derived1>
{
public:
  static void myFunA(int, char const*) { }
  static int myFunB() { return 0; }
};

class Derived2: public Base<Derived2>
{
public:
  static void myFunA(int, char const*) { }
  static int myFunB() { return 1; }
};

int main() {
  Derived1 d1;
  d1.myStruct.funA(0, 0);
  d1.myStruct.funB();
  Derived2 d2;
  d2.myStruct.funA(0, 0);
  d2.myStruct.funB();
}

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

Следствием этого метода является то, что Derived1 и Derived2 не имеют общего базового класса. Два экземпляра Base<> никак не связаны с системой типов. Если вам нужно, чтобы они были связаны друг с другом, тогда вы можете ввести еще один класс, который станет базой для шаблона, а затем разместите там общие вещи:

class RealBase
{
public:
  CStruct myStruct;
};

template <typename T>
class Base: public RealBase
{
  // ...
};

int main()
  RealBase* b;
  Derived1 d1;
  b = &d1;
  b->myStruct.funA(0, 0);
  b->myStruct.funB();
  Derived2 d2;
  b = &d2;
  b->myStruct.funA(0, 0);
  b->myStruct.funB();
}

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

Ответ 2

Думаю, вам просто нужно использовать обычную виртуальную функцию. Статическая виртуальная функция не имеет смысла, поскольку виртуальная функция разрешается во время выполнения. Что там решить, когда компилятор точно знает, что такое статическая функция?

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

Ответ 3

Я все еще вижу использование для статических виртуальных методов, вот пример:

class File
{
    static virtual std::string extension()  {return "";}
}

class ExecutableFile : public File
{
    // static because every executable has same extension
    static virtual std::string extension()  {return ".exe";}
}


std::string extension = "";

// needing static
extension = ExecutableFile::extension();

// not needing static nor virtual
ExecutableFile exeFile;
extension = exeFile.extension();

// needing virtual
File* pFile = &exeFile;
extension = pFile->extension();

Ответ 4

Общий шаблон при передаче указателя функции (обратного вызова) в SDK SD использует тот факт, что многие из таких функций позволяют параметру void *, который является "пользовательскими данными". Вы можете определить, что ваши обратные вызовы являются простыми глобальными функциями или статическими функциями члена класса. Затем каждый обратный вызов может передать параметр "пользовательские данные" указателю базового класса, чтобы вы могли вызвать функцию-член, которая выполняет работу обратного вызова.

Ответ 5

Вы можете просто передать функции непосредственно в конструктор базового класса:

class Base
{
    Base()(int (*myFunA)(...), int (*myFunB)(...)) 
    { myStruct.funA = funA; myStruct.funB = myFunB; ...}
private:
    CStruct myStruct;
};

class Derived1 : public Base
{
    Derived1() : Base (myFunA, myFunB) {}
    static myFunA(...) {...};
    static myFunB(...) {...};
};

class Derived2 : public Base
{
    Derived2() : Base (myFunA, myFunB) {}
    static myFunA(...) {...};
    static myFunB(...) {...};
};

int main()
{
    Derived1 d1;
    Derived2 d2;
    // Now I have two objects with different functionality
}

Ответ 6

Если производный тип объекта можно определить во время компиляции, вы можете использовать "Curiously Recurring Template Pattern" для достижения статического полиморфизма. При таком подходе вы не ограничиваетесь просто переопределением виртуальных нестатических функций-членов. Статические и функциональные члены - честная игра. Вы даже можете переопределять типы (но размер базового объекта не может быть функцией этих типов).

#include <iostream>
#include <stdint.h>

struct VirtualBase {
    static const char* staticConst;
    static char* staticVar;
    static char* staticFun() { return "original static function"; }
    const char* objectConst;
    char* objectVar;
    virtual char* objectFun() { return "original object function"; }
    typedef int8_t Number;
    VirtualBase():
        objectConst("original object const"),
        objectVar("original object var")
    {}
    void virtual_dump(std::ostream& out=std::cout) {
        out << this->staticConst << std::endl;
        out << this->staticVar << std::endl;
        out << this->staticFun() << std::endl;
        out << this->objectConst << std::endl;
        out << this->objectVar << std::endl;
        out << this->objectFun() << std::endl;
        out << "sizeof(Number): " << sizeof(Number) << std::endl;
    }
};
const char* VirtualBase::staticConst = "original static const";
char* VirtualBase::staticVar = "original static var";

template <typename Derived>
struct RecurringBase: public VirtualBase {
    void recurring_dump(std::ostream& out=std::cout) {
        out << Derived::staticConst << std::endl;
        out << Derived::staticVar << std::endl;
        out << Derived::staticFun() << std::endl;
        out << static_cast<Derived*>(this)->staticConst << std::endl;
        out << static_cast<Derived*>(this)->staticVar << std::endl;
        out << static_cast<Derived*>(this)->staticFun() << std::endl;
        out << static_cast<Derived*>(this)->objectConst << std::endl;
        out << static_cast<Derived*>(this)->objectVar << std::endl;
        out << static_cast<Derived*>(this)->objectFun() << std::endl;
        out << "sizeof(Number): " << sizeof(typename Derived::Number) << std::endl;
    }
};

struct Defaults : public RecurringBase<Defaults> {
};

struct Overridden : public RecurringBase<Overridden> {
    static const char* staticConst;
    static char* staticVar;
    static char* staticFun() { return "overridden static function"; }
    const char* objectConst;
    char* objectVar;
    char* objectFun() { return "overridden object function"; }
    typedef int64_t Number;
    Overridden():
        objectConst("overridden object const"),
        objectVar("overridden object var")
    {}
};
const char* Overridden::staticConst = "overridden static const";
char* Overridden::staticVar = "overridden static var";

int main()
{
    Defaults defaults;
    Overridden overridden;
    defaults.virtual_dump(std::cout << "defaults.virtual_dump:\n");
    overridden.virtual_dump(std::cout << "overridden.virtual_dump:\n");
    defaults.recurring_dump(std::cout << "defaults.recurring_dump:\n");
    overridden.recurring_dump(std::cout << "overridden.recurring_dump:\n");
}

Вот результат:

defaults.virtual_dump:
original static const
original static var
original static function
original object const
original object var
original object function
sizeof(Number): 1
overridden.virtual_dump:
original static const
original static var
original static function
original object const
original object var
overridden object function
sizeof(Number): 1
defaults.recurring_dump:
original static const
original static var
original static function
original static const
original static var
original static function
original object const
original object var
original object function
sizeof(Number): 1
overridden.recurring_dump:
overridden static const
overridden static var
overridden static function
overridden static const
overridden static var
overridden static function
overridden object const
overridden object var
overridden object function
sizeof(Number): 8

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

Ответ 7

Предполагая, что C SDK позволяет передать ему void * в ваши данные (и вы должны передать ему этот указатель для производного класса:)

class Base {

  public:

    void Initialize() { /* Pass /this/ and a pointer to myFuncAGate to your C SDK */ }

    virtual myFuncA()=0;

    // This is the method you pass to the C SDK:
    static myFuncAGate(void *user_data) {
        ((Base*)user_data)->myFuncA();
    }
};


class Derived1: public Base {
  public:
    virtual myFuncA() { ... } // This gets called by myFuncAGate()
};

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

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

Ответ 8

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

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

Но, вернувшись в действительно старые времена перед современным С++ (так мне сказали), вы должны были установить эту магию самостоятельно. И это все еще имеет место для виртуальной статики. Хорошей новостью является то, что vtable, который вы создаете вручную для них, на самом деле проще, чем "обычный", его записи никоим образом не дороже - включая пространство и производительность - чем те, которые используются для функций-членов. Просто определите базовый класс с помощью набора указателей функций EXPLICIT (статический vtable) для поддерживаемых API:

template<typename T>
class VirtualStaticVtable {
private:
   typedef T (*StaticFactory)(KnownInputParameters params);

   StaticFactory factoryAPI;  // The 1 and only entry in my static v-table

protected:
   VirtualStaticVtable(StaticFactory factoryApi) : factoryAPI(factoryApi) {}
   virtual ~VirtualStaticVtable() {}
};

Теперь каждый объект, который должен поддерживать статический метод factory, может быть получен из этого класса. Они спокойно передают свой собственный factory своему конструктору и добавляют только 1 указатель на размеры результирующих объектов (как и обычная запись VTable).

Strousup и co. все же могли бы добавить этот идиоматический шаблон на основной язык, если захотят. Это было бы не так сложно. Каждый объект в таком "С+++" просто имел бы 2 vtables вместо 1- 1 для функций-членов, принимающих 'this' в качестве аргумента и 1 для обычных указателей функций. Однако до этого дня мы застряли с ручными vtables, как и старые C-программисты были в дни до С++.

Ответ 9

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

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

Ответ 10

Очевидный способ такой: FillPointers реализован в каждом производном классе.

class Base
{
private:
    CStruct myStruct;
};

class Derived1 : public Base
{
 private:
    static FillPointers() { myStruct.funA = myFunA; myStruct.funB = myFunB; ...}
    Derived1() {  FillPointers();  }
    static myFunA(...) {...};
    static myFunB(...) {...};
};

Однако вы, вероятно, можете избежать использования магии шаблонов...

Ответ 11

Если SDK SD хочет, чтобы вы выполняли операции без предоставления пользовательских данных, тогда объектная ориентация, скорее всего, не нужна, и вы должны просто написать некоторые функции. Еще раз, время, чтобы найти новый SDK.

Ответ 12

class Base
{
    template<class T>
    FillPointers(T* dummy) { myStruct.funA = T::myFunA; myStruct.funB = T::myFunB; ...}
private:
    CStruct myStruct;
};

class Derived1 : public Base
{
    Derived1() {  FillPointers(this);  }
    static myFunA(...) {...};
    static myFunB(...) {...};
};

class Derived2 : public Base
{
    Derived2() {  FillPointers(this);  }
    static myFunA(...) {...};
    static myFunB(...) {...};
};

int main()
{
    Derived1 d1;
    Derived2 d2;
    // Now I have two objects with different functionality
}

см. также статические виртуальные элементы С++?