提问人:Baiyan Huang 提问时间:2/24/2009 最后编辑:Baiyan Huang 更新时间:3/14/2016 访问量:3490
如何在对象切片时生成编译器警告/错误
How to generate a compiler warning/error when object sliced
问:
我想知道是否可以让编译器发出代码的警告/错误,如下所示:
注意:
1. 是的,这是糟糕的编程风格,我们应该避免这种情况 - 但我们正在处理遗留代码,希望编译器可以帮助我们识别此类情况。
2. 我更喜欢编译器选项 (VC++) 来禁用或启用对象切片(如果有的话)。
class Base{};
class Derived: public Base{};
void Func(Base)
{
}
//void Func(Derived)
//{
//
//}
//main
Func(Derived());
在这里,如果我注释掉第二个函数,第一个函数将被调用 - 编译器(VC++ 和 Gcc)对此感到满意。
是C++标准吗?当遇到这样的代码时,我可以要求编译器 (VC++) 给我警告吗?
非常感谢!!
编辑:
非常感谢大家的帮助!
我找不到一个编译器选项来给出错误/警告 - 我什至在 MSDN 论坛上发布了这个 VC++ 编译器顾问,但没有答案。所以恐怕 gcc 和 vc++ 都没有实现这个功能。
因此,添加将派生类作为参数的构造函数将是目前最好的解决方案。
编辑
我已向 MS 提交了 feedbak,希望他们能尽快修复它:
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=421579
-白燕
答:
这通常被称为对象切片,是一个众所周知的问题,可以有自己的维基百科文章(尽管它只是对问题的简短描述)。
我相信我使用了一个编译器,该编译器具有警告,您可以启用该警告来检测和警告此问题。但是,我不记得是哪一个。
不是真正解决你眼前的问题,但是......
大多数将类/结构对象作为参数的函数都应该声明参数为“const X&”或“X&”类型,除非它们有很好的理由不这样做。
如果您始终这样做,则对象切片将永远不会成为问题(引用不会被切片!
评论
class Derived: public Base{};
你说派生是一个基数,所以它应该在任何取基的函数中工作。如果这是一个真正的问题,也许继承不是你真正想要使用的。
评论
我建议在基类中添加一个构造函数,该构造函数显式地引用派生类(带有正向声明)。在我的简单测试应用程序中,这个构造函数在切片情况下被调用。然后,您至少可以得到一个运行时断言,并且您可能可以通过巧妙地使用模板来获得编译时断言(例如:以在该构造函数中生成编译时断言的方式实例化模板)。在调用显式函数时,可能还有特定于编译器的方法来获取编译时警告或错误;例如,可以在 Visual Studio 中对“slice 构造函数”使用“__declspec(已弃用)”来获取编译时警告,至少在函数调用情况下是这样。
因此,在示例中,代码如下所示(对于 Visual Studio):
class Base { ...
__declspec(deprecated) Base( const Derived& oOther )
{
// Static assert here if possible...
}
...
这在我的测试中有效(编译时警告)。请注意,它不能解决复制情况,但类似构造的赋值运算符应该在那里解决问题。
希望这会有所帮助。:)
评论
如果您可以修改基类,则可以执行如下操作:
class Base
{
public:
// not implemented will cause a link error
Base(const Derived &d);
const Base &operator=(const Derived &rhs);
};
根据你的编译器,它应该为你提供翻译单元,也许还有切片发生的函数。
评论
= delete
解决这个问题的最好方法通常是遵循Scott Meyer的建议(参见有效的C++),即只在继承树的叶节点上具有具体的类,并通过至少具有一个纯虚函数(析构函数,如果没有其他的话)来确保非叶类是抽象的。
令人惊讶的是,这种方法也经常以其他方式帮助澄清设计。在任何情况下,隔离一个通用抽象接口的努力通常都是值得的设计工作。
编辑
虽然我最初没有说清楚,但我的答案来自这样一个事实,即在编译时不可能准确地警告对象切片,因此,如果您启用了编译时断言或编译器警告,可能会导致错误的安全感。如果您需要了解对象切片的实例并需要更正它们,那么这意味着您有更改遗留代码的愿望和能力。如果是这样的话,那么我认为你应该认真考虑重构类层次结构,作为使代码更健壮的一种方式。
我的理由是这样的。
考虑一些定义类 Concrete1 并在此函数的推理中使用它的库代码。
void do_something( const Concrete1& c );
传递类型 be 引用是为了提高效率,一般来说,这是一个好主意。如果库将 Concrete1 视为值类型,则实现可能会决定创建输入参数的副本。
void do_something( const Concrete1& c )
{
// ...
some_storage.push_back( c );
// ...
}
如果传递的引用的对象类型确实是某种派生类型,而不是其他派生类型,则此代码很好,不会执行切片。有关此函数调用的一般警告可能只产生误报,并且很可能无济于事。Concrete1
push_back
考虑一些客户端代码,该代码派生自另一个函数并将其传递给另一个函数。Concrete2
Concrete1
void do_something_else( const Concrete1& c );
由于该参数是通过引用获取的,因此此处不会对要检查的参数进行切片,因此在此处警告切片是不正确的,因为可能没有切片发生。将派生类型传递给采用引用或指针的函数是利用多态类型的一种常见且有用的方法,因此警告或禁止这样做似乎会适得其反。
那么哪里有错误呢?好吧,“错误”是传递对从类派生的东西的引用,然后被调用的函数将其视为值类型。
一般来说,没有办法针对对象切片生成一致有用的编译时警告,这就是为什么在可能的情况下,最好的防御措施是通过设计来消除问题。
评论
作为 Andrew Khosravian 回答的变体,我建议使用模板化的复制构造函数和赋值运算符。这样,您就不需要知道给定基类的所有派生类来保护该基类免受切片:
class Base
{
private: // To force a compile error for non-friends (thanks bk1e)
// Not implemented, so will cause a link error for friends
template<typename T> Base(T const& d);
template<typename T> Base const& operator=(T const& rhs);
public:
// You now need to provide a copy ctor and assignment operator for Base
Base(Base const& d) { /* Initialise *this from d */ }
Base const& operator=(Base const& rhs) { /* Copy d to *this */ }
};
尽管这减少了所需的工作量,但使用这种方法,您仍然需要弄乱每个基类以保护它。此外,如果从 到 雇用 的成员进行合法的转换,也会导致问题。(在这种情况下,可以使用更详细的解决方案。在任何情况下,一旦确定了对象切片的所有实例,就应删除此代码。Base
SomeOtherClass
operator Base()
SomeOtherClass
boost::disable_if<is_same<T, SomeOtherClass> >
致全世界的编译器实现者:测试对象切片绝对是值得的,可以创建(可选)警告!我想不出一个需要行为的例子,这在新手 C++ 代码中很常见。
[编辑 2015/3/27:]正如 Matt McNab 所指出的,您实际上不需要像上面那样显式声明复制构造函数和赋值运算符,因为它们仍将由编译器隐式声明。在 2003 年 C++ 标准中,12.8/2 下的脚注 106 明确提到了这一点:
由于模板构造函数从来都不是复制构造函数,因此此类模板的存在不会禁止复制构造函数的隐式声明。模板构造函数与其他构造函数(包括复制构造函数)一起参与重载解析,如果模板构造函数提供比其他构造函数更好的匹配,则可以使用模板构造函数来复制对象。
评论
我稍微修改了你的代码:
class Base{
public:
Base() {}
explicit Base(const Base &) {}
};
class Derived: public Base {};
void Func(Base)
{
}
//void Func(Derived)
//{
//
//}
//main
int main() {
Func(Derived());
}
explicit 关键字将确保构造函数不会用作隐式转换运算符 - 当您想要使用它时,您必须显式调用它。
评论