C++中的静态虚拟成员?

182

在C++中是否有可能创建既是static又是virtual的成员函数?显然,没有一种直接的方法可以做到这一点(static virtual member();会编译错误),但是是否至少有一种实现相同效果的方法呢?

例如:

struct Object
{
     struct TypeInformation;

     static virtual const TypeInformation &GetTypeInformation() const;
};

struct SomeObject : public Object
{
     static virtual const TypeInformation &GetTypeInformation() const;
};

在实例(object->GetTypeInformation())和类(SomeObject::GetTypeInformation())上使用GetTypeInformation()非常有意义,这可以对比很有用,并且对于模板非常重要。

我能想到的唯一方式涉及每个类编写两个函数/一个函数和一个常量,或者使用宏。

还有其他解决方案吗?


14
仅做一个旁注:静态方法不在任何实例上执行,这意味着它们没有隐式的 this 指针。话虽如此,在方法签名中的 const 标识隐式的 this 指针为常量,并且不能应用于静态方法,因为它们缺少隐式参数。 - David Rodríguez - dribeas
2
@cvb:我认为你应该认真考虑用不涉及反射的代码替换你的示例。现在的情况是,你有点混淆了两个不同(虽然相关)的问题。是的,我知道你提出这个问题已经五年半了。 - einpoklum
这里隐含需要的一个特性是编译器检查层级结构中的每个对象是否实现了特定接口(其中一个或多个方法是静态的)。基本上,对于静态方法的纯虚拟检查非常有意义,因为如果您忘记添加静态方法,则编译器应该出错。 virtual不是关键字,它更加“抽象”,在C ++中这种情况发生了一些同义词。 不幸的是,目前无法使用C ++完成此操作。 - xryl669
@cvb 你尝试使用静态接口了吗?https://github.com/ilciavo/static_interface_pattern/blob/master/static_interface/main.cpp - ilciavo
20个回答

90

不行,无法做到,因为当你调用 Object::GetTypeInformation() 时会发生什么?它无法知道要调用哪个派生类的版本,因为没有与其关联的对象。

你必须使其成为非静态虚函数才能正常工作;如果你还想在没有对象实例的情况下调用特定派生类的版本,你还需要提供第二个冗余的静态非虚拟版本。


11
如果你将静态类(或静态成员)看作单例模式,那么一切都会变得清晰明了 - 在你的情况下,只需要调用 Object::GetTypeInformation - 就像在基类实例上调用常规虚拟方法一样。(当然,如果 C++ 支持虚拟静态方法) - Spook
25
那是一个完全站不住脚的论点。如果使用类而不是对象,它自然会使用该类中的版本,而不是进行虚函数派发。这方面没有任何新东西。 - Deduplicator

60

许多人认为它是不可能的,我会更进一步地说这是没有意义的。

静态成员是指与任何实例无关,只与类相关的东西。

虚拟成员是指与任何类直接无关,只与实例相关的东西。

因此,静态虚拟成员将是一些既与任何实例无关,也与任何类无关的东西。


64
在支持将类作为一等值得语言中,这个说法是完全有意义的——例如 Delphi 就支持这种语言特性,并且也有 "静态虚拟" 方法。 - Pavel Minaev
12
我认为静态虚函数是有意义的。可以定义接口类,并包含必须在派生类中实现的静态方法。 - bkausbk
54
对于一个 static virtual 方法来说,这样做并没有太多意义,但是对于接口中的一个 static pure virtual 方法来说,就非常有意义了。 - Bret Kuhns
6
拥有一个 static const string MyClassSillyAdditionalName 是完全有意义的。 - einpoklum
9
如果你的意图是使用编译器确保在所有子类中都实现了静态方法,那么这句话是完全有意义的。@BretKuhns 是正确的。 - xryl669
显示剩余4条评论

26

我最近遇到了一个问题:我有一些类中有许多静态方法,但我想要使用继承和虚方法,以减少代码重复。我的解决方案是:

不要使用静态方法,使用一个带有虚方法的单例。

换句话说,每个类应该包含一个静态方法,您调用该方法以获取指向类的单个共享实例的指针。您可以将真正的构造函数设置为私有或受保护的,以防止外部代码通过创建其他实例来误用它。

在实践中,使用单例与使用静态方法非常相似,只是您可以利用继承和虚方法。


这将会影响我的性能 - 除非编译器可以确定:1.它确实是单例的,2.没有任何继承自它的东西,否则我认为它无法优化所有的开销。 - einpoklum
如果你担心这种事情的性能,那么C#可能不是适合你的编程语言。 - Nate C-K
3
好的,明白了。这显然是我很久以前写的东西了,所以我需要重新表达一下:如果你担心这种性能问题,那么也许你应该完全避免使用继承。发帖者明确要求虚拟方法,所以你来抱怨虚拟方法的开销有些奇怪。 - Nate C-K

18
虽然Alsk已经给出了一个相当详细的答案,但我想补充一种替代方案,因为我认为他的改进实现过于复杂。我们从一个抽象的基类开始,它为所有对象类型提供接口:
class Object
{
public:
    virtual char* GetClassName() = 0;
};

现在我们需要一个实际的实现。但是为了避免编写静态方法和虚拟方法,我们将使实际对象类继承虚拟方法。这显然只有在基类知道如何访问静态成员函数时才有效。因此,我们需要使用模板并将实际对象类的名称传递给它:

template<class ObjectType>
class ObjectImpl : public Object
{
public:
    virtual char* GetClassName()
    {
        return ObjectType::GetClassNameStatic();
    }
};

最后我们需要实现我们的真正对象。在这里,我们只需要实现静态成员函数,虚拟成员函数将从ObjectImpl模板类继承,并使用派生类的名称实例化,因此它将访问其静态成员。

class MyObject : public ObjectImpl<MyObject>
{
public:
    static char* GetClassNameStatic()
    {
        return "MyObject";
    }
};

class YourObject : public ObjectImpl<YourObject>
{
public:
    static char* GetClassNameStatic()
    {
        return "YourObject";
    }
};

让我们添加一些代码来测试:

char* GetObjectClassName(Object* object)
{
    return object->GetClassName();
}

int main()
{
    MyObject myObject;
    YourObject yourObject;

    printf("%s\n", MyObject::GetClassNameStatic());
    printf("%s\n", myObject.GetClassName());
    printf("%s\n", GetObjectClassName(&myObject));
    printf("%s\n", YourObject::GetClassNameStatic());
    printf("%s\n", yourObject.GetClassName());
    printf("%s\n", GetObjectClassName(&yourObject));

    return 0;
}

补充说明(2019年1月12日):

不必使用GetClassNameStatic()函数,您也可以将类名称定义为静态成员,甚至“内联”,自C++11以来已经可以工作(不要被所有修饰符吓到:)):

class MyObject : public ObjectImpl<MyObject>
{
public:
    // Access this from the template class as `ObjectType::s_ClassName` 
    static inline const char* const s_ClassName = "MyObject";

    // ...
};

18

这是可能的!

但具体来说,我们需要缩小范围。人们经常想要某种“静态虚函数”,因为需要复制代码才能通过静态调用“SomeDerivedClass :: myfunction()”和多态调用“base_class_pointer-> myfunction()”来调用相同的函数。允许这种功能的“合法”方法是复制函数定义:

class Object
{
public:
    static string getTypeInformationStatic() { return "base class";}
    virtual string getTypeInformation() { return getTypeInformationStatic(); }
}; 
class Foo: public Object
{
public:
    static string getTypeInformationStatic() { return "derived class";}
    virtual string getTypeInformation() { return getTypeInformationStatic(); }
};

如果基类有大量静态函数,派生类需要覆盖每一个函数,但是忘记为虚函数提供重复定义会怎么样呢?没错,在运行时我们将得到一些奇怪的错误,这很难追踪。因为代码的重复是一件坏事。下面尝试解决这个问题(我想预先说明的是,它完全是类型安全的,不含任何像typeid或dynamic_cast这样的黑魔法 :))。
所以,我们希望每个派生类仅提供一个getTypeInformation()的定义,而且很明显它必须是一个静态函数的定义,因为如果getTypeInformation()是虚函数,就无法调用"SomeDerivedClass::getTypeInformation()"。我们如何通过指向基类的指针调用派生类的静态函数呢?使用vtable是不可能的,因为vtable只存储指向虚函数的指针,既然我们决定不使用虚函数,我们就不能修改vtable以达到我们的目的。因此,为了能够通过指向基类的指针访问派生类的静态函数,我们必须以某种方式在其基类中存储对象的类型。一种方法是使用“奇异递归模板模式”使基类成为模板,但在这里不适用,我们将使用一种称为“类型抹消”的技术:
class TypeKeeper
{
public:
    virtual string getTypeInformation() = 0;
};
template<class T>
class TypeKeeperImpl: public TypeKeeper
{
public:
    virtual string getTypeInformation() { return T::getTypeInformationStatic(); }
};

现在我们可以使用变量“keeper”在基类“Object”中存储对象的类型:
class Object
{
public:
    Object(){}
    boost::scoped_ptr<TypeKeeper> keeper;

    //not virtual
    string getTypeInformation() const 
    { return keeper? keeper->getTypeInformation(): string("base class"); }

};

在派生类中,keeper必须在构造函数中初始化:
class Foo: public Object
{
public:
    Foo() { keeper.reset(new TypeKeeperImpl<Foo>()); }
    //note the name of the function
    static string getTypeInformationStatic() 
    { return "class for proving static virtual functions concept"; }
};

让我们添加语法糖:

template<class T>
void override_static_functions(T* t)
{ t->keeper.reset(new TypeKeeperImpl<T>()); }
#define OVERRIDE_STATIC_FUNCTIONS override_static_functions(this)

现在后代的声明看起来像这样:

class Foo: public Object
{
public:
    Foo() { OVERRIDE_STATIC_FUNCTIONS; }
    static string getTypeInformationStatic() 
    { return "class for proving static virtual functions concept"; }
};

class Bar: public Foo
{
public:
    Bar() { OVERRIDE_STATIC_FUNCTIONS; }
    static string getTypeInformationStatic() 
    { return "another class for the same reason"; }
};

用法:

Object* obj = new Foo();
cout << obj->getTypeInformation() << endl;  //calls Foo::getTypeInformationStatic()
obj = new Bar();
cout << obj->getTypeInformation() << endl;  //calls Bar::getTypeInformationStatic()
Foo* foo = new Bar();
cout << foo->getTypeInformation() << endl; //calls Bar::getTypeInformationStatic()
Foo::getTypeInformation(); //compile-time error
Foo::getTypeInformationStatic(); //calls Foo::getTypeInformationStatic()
Bar::getTypeInformationStatic(); //calls Bar::getTypeInformationStatic()

优点:

  1. 减少代码重复(但我们必须在每个构造函数中调用OVERRIDE_STATIC_FUNCTIONS)

缺点:

  1. 在每个构造函数中调用OVERRIDE_STATIC_FUNCTIONS
  2. 内存和性能开销
  3. 增加了复杂性

未解决的问题:

1) 静态函数和虚拟函数有不同的名称,如何解决歧义?

class Foo
{
public:
    static void f(bool f=true) { cout << "static";}
    virtual void f() { cout << "virtual";}
};
//somewhere
Foo::f(); //calls static f(), no ambiguity
ptr_to_foo->f(); //ambiguity

2) 如何在每个构造函数中隐式调用OVERRIDE_STATIC_FUNCTIONS?


+1 鼓励一下,虽然我不确定这是否比将功能委托给具有虚拟方法的单例更加优雅。 - einpoklum
2
@einpoklum,我可以想到一种情况,这可能更可取。假设我们有很多客户端代码已经调用静态方法。从静态方法切换到具有虚拟方法的单例需要更改客户端代码,而上面提出的解决方案是非侵入性的。 - Alsk
“virtual” 关键字在 “Foo::getTypeInformation” 和 “TypeKeeperImpl::getTypeInformation” 中不是必需的。 - bartolo-otrit

12

虽然有些人会这样说,但实际上这是一个遗漏的问题,而不是什么“毫无意义”的事情。明确一下,我所指的是类似以下这种情况:

struct Base {
  static virtual void sayMyName() {
    cout << "Base\n";
  }
};

struct Derived : public Base {
  static void sayMyName() override {
    cout << "Derived\n";
  }
};

void foo(Base *b) {
  b->sayMyName();
  Derived::sayMyName(); // Also would work.
}

这是100%可以实现的东西(只是尚未实现),我认为这是非常有用的。
考虑一下普通虚函数的工作方式。移除static并添加其他内容,我们有:
struct Base {
  virtual void sayMyName() {
    cout << "Base\n";
  }
  virtual void foo() {
  }
  int somedata;
};

struct Derived : public Base {
  void sayMyName() override {
    cout << "Derived\n";
  }
};

void foo(Base *b) {
  b->sayMyName();
}

这个很好地工作,基本上的情况是编译器制作了两个表,称为VTables,并为虚函数分配索引,如下所示

enum Base_Virtual_Functions {
  sayMyName = 0;
  foo = 1;
};

using VTable = void*[];

const VTable Base_VTable = {
  &Base::sayMyName,
  &Base::foo
};

const VTable Derived_VTable = {
  &Derived::sayMyName,
  &Base::foo
};

接下来,每个带有虚函数的类都会增加另一个指向其VTable的字段,因此编译器基本上会将它们更改为以下形式:

struct Base {
  VTable* vtable;
  virtual void sayMyName() {
    cout << "Base\n";
  }
  virtual void foo() {
  }
  int somedata;
};

struct Derived : public Base {
  VTable* vtable;
  void sayMyName() override {
    cout << "Derived\n";
  }
};

当您调用b->sayMyName()时,实际上会发生什么?基本上是这样的:
b->vtable[Base_Virtual_Functions::sayMyName](b);

第一个参数成为this

好的,那么静态虚函数如何工作呢?静态成员函数和非静态成员函数有什么区别?唯一的区别就是后者会获得一个this指针。

我们可以完全按照同样的方式处理静态虚函数 - 只需删除this指针即可。

b->vtable[Base_Virtual_Functions::sayMyName]();

这样做可以支持两种语法:

b->sayMyName(); // Prints "Base" or "Derived"...
Base::sayMyName(); // Always prints "Base".

所以忽略所有的批评者。这是有意义的。为什么它没有得到支持呢?我认为这是因为它的好处非常少,甚至可能会有点混淆。
与普通虚函数相比,唯一的技术优势是您不需要将“this”传递给函数,但我认为这对性能没有任何可衡量的影响。
这确实意味着当您有一个实例和没有实例的情况下,您不需要单独的静态和非静态函数,但同时也可能让人感到困惑,因为只有在使用实例调用时它才真正是“虚拟”的。

12

可以做到。制作两个函数:静态和虚拟

struct Object{     
  struct TypeInformation;
  static  const TypeInformation &GetTypeInformationStatic() const 
  { 
      return GetTypeInformationMain1();
  }
  virtual const TypeInformation &GetTypeInformation() const
  { 
      return GetTypeInformationMain1();
  }
protected:
  static const TypeInformation &GetTypeInformationMain1(); // Main function
};

struct SomeObject : public Object {     
  static  const TypeInformation &GetTypeInformationStatic() const 
  { 
      return GetTypeInformationMain2();
  }
  virtual const TypeInformation &GetTypeInformation() const
  { 
      return GetTypeInformationMain2();
  }
protected:
  static const TypeInformation &GetTypeInformationMain2(); // Main function
};

4
此外,静态方法不能是const。这没有意义,它们不会改变任何实例,因为它们不存在实例。 - David Rodríguez - dribeas
2
这主要是代码重复。其想法是子类只需要拥有静态常量成员,而不必拥有访问它的代码。 - einpoklum

9
不可能,因为静态成员函数缺少“this”指针。而静态成员(包括函数和变量)并不是真正的类成员。它们只是通过“ClassName::member”调用,并遵守类访问限定符。它们的存储位置在类的外部定义;存储空间并不会在每次实例化类对象时创建。类成员指针在语义和语法上具有特殊性。静态成员指针在所有方面都是普通指针。
类中的虚函数需要“this”指针,并且与类非常紧密相关,因此不能是静态的。

1
只有非静态函数需要 this 指针。静态函数不特定于实例,因此不需要它。因此,这并不是虚拟静态成员不可能的原因。 - einpoklum

7

很晚回答,但是使用奇异递归模板模式是可能的。这个维基百科文章有你需要的信息,而且静态多态性下的例子正是你所要求的。


4
这个问题已经有十年历史了,但看起来它仍然受到很多人的关注,因此我想发布一个使用现代C++特性的替代方案,这是我在其他任何地方都没有看到过的。
这个解决方案使用CRTP和SFINAE进行静态分派。这本身并不新颖,但我发现所有这样的实现都缺乏对于“重载”方法的严格签名检查。这个实现要求“重载”方法的签名与“被重载”方法完全匹配。这种行为更类似于虚函数,同时也允许我们有效地重载和“覆盖”静态方法。
需要注意的是,我在引号中放置了override,因为严格来说,我们并没有技术上的重载任何东西。相反,我们将具有签名Y的调度方法X与T::X一起调用,其中T是一组类型中的第一个类型,这些类型中存在具有签名Y的T::X。考虑用于调度的类型列表可以是任何内容,但通常包括默认实现类和派生类。 实现
#include <experimental/type_traits>

template <template <class...> class Op, class... Types>
struct dispatcher;

template <template <class...> class Op, class T>
struct dispatcher<Op, T> : std::experimental::detected_t<Op, T> {};

template <template <class...> class Op, class T, class... Types>
struct dispatcher<Op, T, Types...>
  : std::experimental::detected_or_t<
    typename dispatcher<Op, Types...>::type, Op, T> {};


// Helper to convert a signature to a function pointer
template <class Signature> struct function_ptr;

template <class R, class... Args> struct function_ptr<R(Args...)> {
    using type = R (*)(Args...);
};


// Macro to simplify creation of the dispatcher
// NOTE: This macro isn't smart enough to handle creating an overloaded
//       dispatcher because both dispatchers will try to use the same
//       integral_constant type alias name. If you want to overload, do it
//       manually or make a smarter macro that can somehow put the signature in
//       the integral_constant type alias name.
#define virtual_static_method(name, signature, ...)                            \
    template <class VSM_T>                                                     \
    using vsm_##name##_type = std::integral_constant<                          \
        function_ptr<signature>::type, &VSM_T::name>;                          \
                                                                               \
    template <class... VSM_Args>                                               \
    static auto name(VSM_Args&&... args)                                       \
    {                                                                          \
        return dispatcher<vsm_##name##_type, __VA_ARGS__>::value(              \
            std::forward<VSM_Args>(args)...);                                  \
    }

例子用法

#include <iostream>

template <class T>
struct Base {
    // Define the default implementations
    struct defaults {
        static std::string alpha() { return "Base::alpha"; };
        static std::string bravo(int) { return "Base::bravo"; }
    };

    // Create the dispatchers
    virtual_static_method(alpha, std::string(void), T, defaults);
    virtual_static_method(bravo, std::string(int), T, defaults);
    
    static void where_are_the_turtles() {
        std::cout << alpha() << std::endl;  // Derived::alpha
        std::cout << bravo(1) << std::endl; // Base::bravo
    }
};

struct Derived : Base<Derived> {
    // Overrides Base::alpha
    static std::string alpha(){ return "Derived::alpha"; }

    // Does not override Base::bravo because signatures differ (even though
    // int is implicitly convertible to bool)
    static std::string bravo(bool){ return "Derived::bravo"; }
};

int main() {
    Derived::where_are_the_turtles();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接