我正在寻找一种检查子类是否覆盖其基类上的函数的方法。如果成员函数指针不是虚拟的,则比较成员函数指针可以很好地工作,但是如果它们是虚拟的,则无法工作。这个示例代码本质上就是我遇到的麻烦。
class Base {
public:
virtual void vfoo(){ cout << "foo"; }
virtual void vbar(){ cout << "bar"; }
void foo(){ cout << "foo"; }
void bar(){ cout << "bar"; }
};
class Child : public Base {
public:
void vfoo(){ cout << "foo2"; }
void foo(){ cout << "foo2"; }
};
int main (){
//non-virtual cases, these work correctly
cout << (&Base::foo == &Child::foo) << endl; //outputs false (good)
cout << (&Base::bar == &Child::bar) << endl; //outputs true (good)
//virtual cases, these do not work correctly
cout << (&Base::vfoo == &Child::vfoo) << endl; //outputs true (BAD, child::vfoo and base::vfoo are DIFFERENT FUNCTIONS)
cout << (&Base::vbar == &Child::vbar) << endl; //outputs true (good, child::vbar and base::vbar are the same)
return 0;
}
Run Code Online (Sandbox Code Playgroud)
从逻辑上讲,没有理由不应该这样做,但是C ++规范则相反(通过实现定义比较虚拟函数地址)。
在GCC上,键入punning&Base :: vfoo和&Child :: vfoo以将它们都设为“ 1”(而vbar为“ 9”),将它们都设为int,它们看起来是vtable偏移量。以下代码似乎可以正确地从vtable中获取函数地址,并正确报告Child :: vfoo和Base :: bfoo的不同地址,以及vbar的相同地址
template<typename A, typename B>
A force_cast(B in){
union {
A a;
B b;
} u;
u.b = in;
return u.a;
};
template<typename T>
size_t get_vtable_function_address_o(T* obj, int vtable_offset){
return *((size_t*)((*(char**)obj + vtable_offset-1)));
};
template<typename T, typename F>
size_t get_vtable_function_address(T* obj, F function){
return get_vtable_function_address_o(obj, force_cast<size_t>(function));
};
int main (){
Base* a = new Base();
Base* b = new Child();
cout << get_vtable_function_address(a, &Base::vfoo) << endl;
cout << get_vtable_function_address(b, &Base::vfoo) << endl;
cout << get_vtable_function_address(a, &Base::vbar) << endl;
cout << get_vtable_function_address(b, &Base::vbar) << endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
尽管我必须从vtable偏移中减去1才能运行,但这在GCC上仍然可以正常工作。但这在Microsoft的编译器上不起作用(将&Base :: vfoo调整为size_t会返回一些垃圾而不是虚拟表偏移量)(此处的一些实验表明,此处的正确偏移量对于vfoo为0,对于vbar为4)
我很清楚,这些东西是实现定义的,但是我希望至少有一种方法可以在至少一些常见的编译器(gcc,msvc和clang)上运行,因为vtables在这一点上是非常标准的(甚至是否需要特定于编译器的代码)?
有什么办法吗?
注意1:我只需要这个就可以处理单一继承。我不使用多重继承或虚拟继承
注2:再次强调我不需要可调用函数,只需要测试子类是否覆盖了特定的虚函数。如果有一种方法可以做到而无需深入研究vtables,那么那将是首选。
在 C++11 及以上版本中,通过decltype和比较函数类型std::is_same,我们可以得到想要的结果。(如果 C++11 对您不可用,您仍然可以使用typeid和operator==(const type_info& rhs)用于此目的。)
由于Base::vfoo通过覆盖Child的类型,decltype(&Child::vfoo)就是void (Child::*)()不同于和decltype(&Base::vfoo)其void (Base::*)()。因此
std::is_same<decltype(&Base::vfoo) , decltype(&Child::vfoo)>::value
Run Code Online (Sandbox Code Playgroud)
是false。
(实际上,在枚举隐式转换集的 C++ 标准草案 n3337 的第 4 条中,4.11 指向成员转换的指针 [conv.mem] / 2,
- “指向 cv T 类型 B 成员的指针”类型的纯右值,其中 B 是类类型,可以转换为“指向类型 cv T 的 D 成员的指针”类型的纯右值,其中 D 是派生类( B 的第 10 条。如果 B 是 D 的不可访问(第 11 条)、二义性(10.2)或虚拟(10.1)基类,或 D 的虚拟基类的基类,则需要这种转换的程序是畸形。转换的结果引用与转换发生之前指向成员的指针相同的成员,但它引用基类成员,就好像它是派生类的成员一样。结果引用了 D 的 B 实例中的成员。由于结果的类型为“指向 cv T 类型的 D 成员的指针”,因此可以使用 D 对象取消引用它。结果与指向 B 成员的指针被 D 的 B 子对象解除引用一样。
, 表示从decltype(&Base::vfoo)to的隐式转换decltype(&Child::vfoo)可以是合法的,但没有提到反向之一。另外,5.2.9 静态施法 [expr.static.cast] / 12,
- 类型的“指针类型的d的部件A prvalue CV1 T”可被转换为类型的prvalue“指针B的成员”类型的CV2 T,其中B是d的基类(第10)中,如果存在从“指向 T 类型 B 成员的指针”到“指向 T 类型 D 成员的指针”的有效标准转换(4.11),并且cv2与cv1具有相同的 cv 限定,或比cv1更大的 cv 限定。然后空成员指针值(4.11)被转换为目标类型的空成员指针值。如果类 B 包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针指向原始成员。否则,转换的结果是未定义的。[注意:虽然B类不需要包含原始成员,但解除引用成员指针的对象的动态类型必须包含原始成员;见 5.5。—尾注]
, 声明使用static_castfrom decltype(&Child::vfoo)to的显式转换decltype(&Base::vfoo)也可以是合法的。那么在这种情况下彼此的合法强制转换是
void (Child::*pb)() = &Base::vfoo;
void (Base ::*pc)() = static_cast<void(Base::*)()>(&Child::vfoo);
Run Code Online (Sandbox Code Playgroud)
这static_cast意味着该类型的&Base::vfoo和&Child::vfoo没有任何明确的转换相互不同)。
OTOH,因为Base::vbar没有被覆盖Child的类型,decltype(&Child::vbar)就是void (Base::*)()与相同decltype(&Base::vbar)。因此
std::is_same<decltype(&Base::vbar) , decltype(&Child::vbar)>::value
Run Code Online (Sandbox Code Playgroud)
是true。
(似乎是n3337 的 5.3.1 一元运算符 [expr.unary.op] / 3,
一元 & 运算符的结果是指向其操作数的指针。操作数应为左值或限定 ID。如果操作数是一个限定 id命名某个类型为 T 的类 C 的非静态成员 m,则结果的类型为“指向类型 T 的类 C 的成员的指针”,并且是一个指定 C::m 的纯右值。否则,如果表达式的类型是 T,则结果的类型为“指向 T 的指针”,并且是一个纯右值,它是指定对象 (1.7) 的地址或指向指定函数的指针。[注意:特别是,类型为“ cv T”的对象的地址是“指向cv T的指针”,具有相同的cv限定。— 尾注 ] [ 示例:
Run Code Online (Sandbox Code Playgroud)struct A { int i; }; struct B : A { }; ... &B::i ... // has type int A::*— 结束示例 ]
, 说明此行为。还可以在此处找到对本段的有趣讨论。)
综上所述,我们可以检查每个成员功能是否被覆盖或者不使用decltype(&Base::...),decltype(&Child::...)并且std::is_same如下所示:
现场演示 (GCC / Clang / ICC / VS2017)
// Won't fire.
static_assert(!std::is_same<decltype(&Base::foo) , decltype(&Child::foo)> ::value, "oops.");
// Won't fire.
static_assert( std::is_same<decltype(&Base::bar) , decltype(&Child::bar)> ::value, "oops.");
// Won't fire.
static_assert(!std::is_same<decltype(&Base::vfoo), decltype(&Child::vfoo)>::value, "oops.");
// Won't fire.
static_assert( std::is_same<decltype(&Base::vbar), decltype(&Child::vbar)>::value, "oops.");
Run Code Online (Sandbox Code Playgroud)
顺便说一句,我们还可以定义以下宏来使这些更容易:
#define IS_OVERRIDDEN(Base, Child, Func) \
(std::is_base_of<Base, Child>::value \
&& !std::is_same<decltype(&Base::Func), decltype(&Child::Func)>::value)
Run Code Online (Sandbox Code Playgroud)
然后让我们写
static_assert( IS_OVERRIDDEN(Base, Child, foo) , "oops."); // Won't fire.
static_assert(!IS_OVERRIDDEN(Base, Child, bar) , "oops."); // Won't fire.
static_assert( IS_OVERRIDDEN(Base, Child, vfoo), "oops."); // Won't fire.
static_assert(!IS_OVERRIDDEN(Base, Child, vbar), "oops."); // Won't fire.
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1401 次 |
| 最近记录: |