C ++静态虚拟成员?


140

在C ++中,是否有可能既有static和又有成员函数virtual?显然,没有一种简单的方法(static virtual member();编译错误),但是至少有一种方法可以达到相同的效果吗?

IE浏览器:

struct Object
{
     struct TypeInformation;

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

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

GetTypeInformation()在实例(object->GetTypeInformation())和类(SomeObject::GetTypeInformation())上使用都是有意义的,这对于比较很有用,对模板也很重要。

我能想到的唯一方法是针对每个类编写两个函数/一个函数和一个常量,或者使用宏。

还有其他解决方案吗?


12
只是附带说明:静态方法不在任何实例上执行,这意味着它们没有隐式this指针。话虽这么说,const方法中的签名将隐式this指针标记为常量,并且由于缺少隐式参数而无法应用于静态方法。
大卫·罗德里格斯(DavidRodríguez)-德里贝斯

2
@cvb:我会认真考虑重新用不涉及反射的代码替换您的示例。现在,您正在将两个单独的(尽管相关)问题混为一谈。是的,我知道距您提出要求已经过去了五年半。
einpoklum 2015年

这里隐式要求的功能之一是让编译器检查层次结构中的每个对象是否实现了特定的接口(其中一个或多个方法是静态的)。基本上,对静态方法进行纯虚拟检查很有意义,因为如果您忘记添加静态方法,则编译器应该会出错。virtual不是这里的关键字,它更抽象,碰巧是C ++中的同义词,除了这种特殊情况。不幸的是,您目前无法使用C ++做到这一点。
xryl669

Answers:


75

不,没有办法,因为您打电话时会发生什么 Object::GetTypeInformation()?它不知道要调用哪个派生类版本,因为没有与之关联的对象。

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


8
如果您将静态类(或类的静态成员)视为一个单例,那么一切都会变得很明显-在您的情况下,应该简单地调用Object :: GetTypeInformation-与在基类instance上调用常规虚拟方法的方式相同。(当然,如果 C ++支持虚拟静态方法)
惊吓2013年

13
那是一个完全似是而非的论点。如果使用类而不是对象,则自然会使用该类的版本,而不是进行虚拟调度。那里没有新东西。
Deduplicator

54

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

静态成员是仅与类无关的任何实例。

虚拟成员是不与任何类直接相关,而仅与实例直接相关的东西。

因此,静态虚拟成员将与任何实例或任何类都不相关。


42
在类为一等值的语言中,这是非常有意义的-例如,Delphi拥有该值,并且还具有“静态虚拟”方法。
Pavel Minaev 09年

4
究竟。“虚拟功能”(按定义)是动态链接的功能,即在运行时根据给定对象的动态类型进行选择。因此,没有对象=没有虚拟调用。
科斯

7
我也认为静态虚拟是有意义的。可以定义接口类,并包括必须在派生类中实现的静态方法。
bkausbk

34
对于static virtual方法来说,它没有什么意义,但是在接口中,static virtual方法是非常有意义的。
Bret Kuhns 2013年

4
拥有一个static const string MyClassSillyAdditionalName
einpoklum

23

前几天,我遇到了这个问题:我有一些充满静态方法的类,但我想使用继承和虚拟方法并减少代码重复。我的解决方案是:

代替使用静态方法,将单例与虚拟方法一起使用。

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

实际上,使用单例与使用静态方法非常相似,不同之处在于可以利用继承和虚拟方法。


这会降低我的性能-除非编译器可以确定:1.它实际上是一个单例,并且2.没有任何继承,我认为它无法优化所有开销。
einpoklum 2015年

如果这种事情的性能让您感到担心,那么C#可能是您使用的错误语言。
Nate CK

3
嗯,好点。显然,自从我在2009年编写此书以来,就已经考虑了一段时间。让我换一种说法,那就是:如果这种性能问题让您感到担心,那么也许您应该完全避免使用继承。发布者专门要求使用虚拟方法,因此奇怪的是您来这里抱怨虚拟方法的开销。
Nate CK

15

有可能的!

但是,到底有什么可能,让我们缩小范围。人们经常想要某种“静态虚拟函数”,因为能够通过静态调用“ 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()定义,很明显,它必须是 static函数,因为如果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 2014年

1
@einpoklum,我认为这是可取的情况。假设我们有很多客户端代码已经调用了静态方法。从静态方法切换为具有虚拟方法的单例将需要更改客户端代码,而上述解决方案是非侵入性的。
2014年

“ Foo :: getTypeInformation”和“ TypeKeeperImpl :: getTypeInformation”不需要“ virtual”关键字。
bartolo-otrit 2014年

12

尽管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起IIRC就可以使用它(不要被所有修饰符所吓倒):):

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

    // ...
};

11

有可能的。实现两个功能:静态和虚拟

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。只是没有意义,他们不会突变什么情况?
大卫·罗德里格斯(DavidRodríguez)-德里贝斯

1
这主要只是代码重复。想法是子类只需要具有静态const成员,而不必具有访问它的代码。
einpoklum

8

不,这是不可能的,因为静态成员函数缺少this指针。静态成员(函数和变量)本身并不是真正的类成员。它们恰好被调用ClassName::member,并遵守类访问说明符。它们的存储在课程之外的某个地方定义。每次实例化该类的对象时都不会创建存储。指向类成员的指针在语义和语法上是特殊的。在所有方面,指向静态成员的指针都是普通指针。

类中的虚函数需要this指针,并且非常类似于该类,因此它们不能是静态的。


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

7

嗯,答案很晚,但是可以使用奇怪的重复模板模式。这篇Wikipedia文章提供了您所需的信息,并且静态多态下的示例也是您所需要的。


3

我认为您可以尝试通过模板来完成。我正在尝试在这里阅读。您想要做的是从某些代码中调用方法,该方法在其中调用派生版本,但调用者未指定哪个类。例:

class Foo {
public:
    void M() {...}
};

class Bar : public Foo {
public:
    void M() {...}
};

void Try()
{
    xxx::M();
}

int main()
{
    Try();
}

您希望Try()在不指定Bar的情况下调用M的Bar版本。对静态对象执行此操作的方法是使用模板。所以像这样改变它:

class Foo {
public:
    void M() {...}
};

class Bar : public Foo {
public:
    void M() {...}
};

template <class T>
void Try()
{
    T::M();
}

int main()
{
    Try<Bar>();
}

1
如果您将代码缩进4个空格,则可以将其自动格式化。另外,我相信您可以使用反勾来实现内联的相同目的。
chollida

1
这是我很想念的。谢谢。尽管如此,公共成员还是很奇怪。
allesblinkt 2012年

M()不是静态函数。怎么称呼T :: M()?
DDukDDak99 '19

3

不能,静态成员函数不能是虚拟的。因为虚拟概念是在运行时借助vptr解决的,并且vptr是类的非静态成员,因为该静态成员函数无法访问vptr,因此静态成员可以不是虚拟的。


2
仅特定于实例的虚拟方法需要实例的vtable。您可能有一个静态(每类一个)vtable。而且,如果您希望实例知道,只需将实例的vtable也指向类静态vtable。
einpoklum 2016年

2

这是不可能的,但这仅仅是因为遗漏了。许多人似乎声称这不是“没有意义的”事情。明确地说,我正在谈论这样的事情:

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%的东西,可以实现(只是还没有),我要说的东西是有用的。

考虑正常的虚拟功能如何工作。删除statics并添加其他内容,我们有:

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给该功能,但我认为这不会对性能产生任何可衡量的影响。

这确实意味着您在拥有实例和没有实例的情况下没有单独的静态和非静态函数,但是使用它只是真正的“虚拟”可能会令人困惑实例调用。


0

不,这不可能,因为静态成员是在编译时绑定的,而虚拟成员是在运行时绑定的。


0

首先,OP所要求的是对矛盾的回答是正确的:虚拟方法取决于实例的运行时类型;静态函数特别不依赖于实例,而仅依赖于类型。也就是说,让静态函数返回特定于某种类型的东西是有意义的。例如,我有一组用于State模式的MouseTool类,并且我开始让每个类都有一个静态函数,该函数返回与之配套的键盘修饰符。我在制造正确的MouseTool实例的工厂函数中使用了这些静态函数。该函数根据MouseToolA :: keyboardModifier(),MouseToolB :: keyboardModifier()等检查鼠标状态,然后实例化相应的鼠标状态。当然以后,我想检查状态是否正确,所以我想写些类似“

因此,如果您发现自己想要这样做,则可能需要重新考虑您的解决方案。尽管如此,我仍然了解拥有静态方法,然后根据实例的动态类型动态调用它们的愿望。我认为“ 访客模式可以满足您的需求。它给您您想要的。这是一些额外的代码,但对其他访问者可能很有用。

有关背景,请参见:http//en.wikipedia.org/wiki/Visitor_pattern

struct ObjectVisitor;

struct Object
{
     struct TypeInformation;

     static TypeInformation GetTypeInformation();
     virtual void accept(ObjectVisitor& v);
};

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

struct AnotherObject : public Object
{
     static TypeInformation GetTypeInformation();
     virtual void accept(ObjectVisitor& v) const;
};

然后对于每个具体对象:

void SomeObject::accept(ObjectVisitor& v) const {
    v.visit(*this); // The compiler statically picks the visit method based on *this being a const SomeObject&.
}
void AnotherObject::accept(ObjectVisitor& v) const {
    v.visit(*this); // Here *this is a const AnotherObject& at compile time.
}

然后定义基本访问者:

struct ObjectVisitor {
    virtual ~ObjectVisitor() {}
    virtual void visit(const SomeObject& o) {} // Or = 0, depending what you feel like.
    virtual void visit(const AnotherObject& o) {} // Or = 0, depending what you feel like.
    // More virtual void visit() methods for each Object class.
};

然后,选择适当的静态函数的具体访问者:

struct ObjectVisitorGetTypeInfo {
    Object::TypeInformation result;
    virtual void visit(const SomeObject& o) {
        result = SomeObject::GetTypeInformation();
    }
    virtual void visit(const AnotherObject& o) {
        result = AnotherObject::GetTypeInformation();
    }
    // Again, an implementation for each concrete Object.
};

最后,使用它:

void printInfo(Object& o) {
    ObjectVisitorGetTypeInfo getTypeInfo;
    Object::TypeInformation info = o.accept(getTypeInfo).result;
    std::cout << info << std::endl;
}

笔记:

  • 锻炼时要保持恒定。
  • 您从静态返回了引用。除非您有单身人士,否则这是有问题的。

如果要避免复制粘贴错误(其中一种访问方法调用了错误的静态函数),则可以使用带有以下模板的访问者的模板化帮助器函数(本身不能是虚拟的):

struct ObjectVisitorGetTypeInfo {
    Object::TypeInformation result;
    virtual void visit(const SomeObject& o) { doVisit(o); }
    virtual void visit(const AnotherObject& o) { doVisit(o); }
    // Again, an implementation for each concrete Object.

  private:
    template <typename T>
    void doVisit(const T& o) {
        result = T::GetTypeInformation();
    }
};

虚拟静态方法(如果存在)将不依赖实例中的任何内容-但是实例需要知道其类型才能调用它们。这可以由编译器解决(例如,通过使用一些具有虚拟静态方法和成员的指针的每类单个数据结构)。
einpoklum

就术语而言是否矛盾是语义问题。可以想象C ++允许从实例调用静态函数(例如Foo foo; ... foo::bar();而不是Foo::bar();)。这没有什么不同,decltype(foo)::bar();但是又会被静态绑定。访问者方法似乎是获得此行为的合理方法,而不仅仅是使静态方法成为虚拟const方法。
2014年

0

使用c ++时,可以将静态继承与crt方法一起使用。例如,它广泛用于窗口模板atl和wtl。

看到 https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

为简单起见,您有一个从本身开始模板化的类,例如类myclass:public myancestor。从这一点开始,myancestor类现在可以调用您的静态T :: YourImpl函数。


-1

也许您可以在下面尝试我的解决方案:

class Base {
public:
    Base(void);
    virtual ~Base(void);

public:
    virtual void MyVirtualFun(void) = 0;
    static void  MyStaticFun(void) { assert( mSelf != NULL); mSelf->MyVirtualFun(); }
private:
    static Base* mSelf;
};

Base::mSelf = NULL;

Base::Base(void) {
    mSelf = this;
}

Base::~Base(void) {
    // please never delete mSelf or reset the Value of mSelf in any deconstructors
}

class DerivedClass : public Base {
public:
    DerivedClass(void) : Base() {}
    ~DerivedClass(void){}

public:
    virtual void MyVirtualFun(void) { cout<<"Hello, it is DerivedClass!"<<endl; }
};

int main() {
    DerivedClass testCls;
    testCls.MyStaticFun(); //correct way to invoke this kind of static fun
    DerivedClass::MyStaticFun(); //wrong way
    return 0;
}

是的,我知道,四年了。为那些不想阅读这么多代码的人解释-score。Base::mSelf引用任何派生类的最近构造的实例,即使该实例已被销毁。所以class D1 : public Base ...; class D2 : public Base ...; ...; D1* pd1 = new D1(); D2* pd2 = new D2(); pd1->MyStaticFun(); /* calls D2::MyVirtualFun() */ delete pd2; pd1->MyStaticFun(); /* calls via deleted pd2 */这不是想要的。
杰西·奇斯霍尔姆

-3

就像其他人所说的那样,有2条重要的信息:

  1. this进行静态函数调用时没有指针,并且
  2. this指针指向该虚拟表,或形实转换,被用于查找要调用哪个运行方法的结构。

静态函数是在编译时确定的。

在类的C ++静态成员中展示了此代码示例;它表明您可以在给定空指针的情况下调用静态方法:

struct Foo
{
    static int boo() { return 2; }
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo* pFoo = NULL;
    int b = pFoo->boo(); // b will now have the value 2
    return 0;
}

6
从技术上讲,这是未定义的行为。由于任何原因,您都不能引用空指针。您可以使用空指针进行的唯一操作是:a)向其分配另一个指针,以及b)将其与另一个指针进行比较。
KeithB

1
此外,你只能比较其是否相等(或inequality_和其他指针,不排序即p < nullp >= null等都是不确定的为好。
帕维尔Minaev

1
@KeithB-为了完整起见,您还可以安全地在空指针上调用delete。
史蒂夫·罗2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.