如何在对象切片时生成编译器警告/错误

How to generate a compiler warning/error when object sliced

提问人:Baiyan Huang 提问时间:2/24/2009 最后编辑:Baiyan Huang 更新时间:3/14/2016 访问量:3490

问:

我想知道是否可以让编译器发出代码的警告/错误,如下所示:

注意:

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

-白燕

C++ 编译器构造 截断 切片

评论


答:

0赞 Greg Hewgill 2/24/2009 #1

这通常被称为对象切片,是一个众所周知的问题,可以有自己的维基百科文章(尽管它只是对问题的简短描述)。

我相信我使用了一个编译器,该编译器具有警告,您可以启用该警告来检测和警告此问题。但是,我不记得是哪一个。

0赞 Drew Hall 2/24/2009 #2

不是真正解决你眼前的问题,但是......

大多数将类/结构对象作为参数的函数都应该声明参数为“const X&”或“X&”类型,除非它们有很好的理由不这样做。

如果您始终这样做,则对象切片将永远不会成为问题(引用不会被切片!

评论

0赞 j_random_hacker 2/24/2009
虽然这是一个很好的建议,但我必须 -1 这个,因为我们现在知道提问者正在处理一堆遗留代码,所以设计建议是没有帮助的。
0赞 CB Bailey 2/24/2009
虽然这个建议通常很好,但在存在可切片对象层次结构的情况下,它可以将切片推迟到在编译时无法检测到的位置。
0赞 Drew Hall 2/25/2009
@j_random_hacker:我回答的时候并不知道。你说得对,现在对他没有帮助,但我会保留答案,因为它可能会帮助某人在未来避免这个问题。
0赞 Drew Hall 2/25/2009
@Charles Bailey:你能详细说明一下吗?
0赞 j_random_hacker 2/25/2009
@Drew:是的,当提问者遗漏一些关键的花絮,然后在你回答后添加它时,这是一种痛苦......回复:查尔斯的评论,看看他的回答。简而言之,即使完全重新编译,如果单独源文件中的函数采用引用参数,然后将其视为值,则无法检测到切片。
0赞 BigSandwich 2/24/2009 #3
class Derived: public Base{};

你说派生是一个基数,所以它应该在任何取基的函数中工作。如果这是一个真正的问题,也许继承不是你真正想要使用的。

评论

1赞 Greg Hewgill 2/24/2009
在具有按值传递语义的C++中,这是一个真正的问题,因为从Func中调用的Base上的虚函数可能没有完整的派生对象可以使用。
0赞 BigSandwich 2/24/2009
是的,你已经提出来了,但这不是他唯一的问题。他可能在每个函数中都有完全不同的逻辑,而且他似乎并不完全理解继承语义。
1赞 Baiyan Huang 2/24/2009
我们正在处理遗留代码,并且需要在重构时监视此类情况。是的,这不是好的编程风格,我的重点是:有没有办法让编译器生成警告?
0赞 j_random_hacker 2/24/2009
必须 -1 这个,因为我们现在知道提问者正在处理一堆遗留代码,所以设计建议是没有帮助的。
5赞 Nick 2/24/2009 #4

我建议在基类中添加一个构造函数,该构造函数显式地引用派生类(带有正向声明)。在我的简单测试应用程序中,这个构造函数在切片情况下被调用。然后,您至少可以得到一个运行时断言,并且您可能可以通过巧妙地使用模板来获得编译时断言(例如:以在该构造函数中生成编译时断言的方式实例化模板)。在调用显式函数时,可能还有特定于编译器的方法来获取编译时警告或错误;例如,可以在 Visual Studio 中对“slice 构造函数”使用“__declspec(已弃用)”来获取编译时警告,至少在函数调用情况下是这样。

因此,在示例中,代码如下所示(对于 Visual Studio):

class Base { ...
    __declspec(deprecated) Base( const Derived& oOther )
    {
        // Static assert here if possible...
    }
...

这在我的测试中有效(编译时警告)。请注意,它不能解决复制情况,但类似构造的赋值运算符应该在那里解决问题。

希望这会有所帮助。:)

评论

1赞 Baiyan Huang 2/24/2009
谢谢尼克,这真的很酷!但就我而言,我什至不知道哪些类有这样的问题 - 我需要编译器来帮助我在大量代码中找到这种情况。-白燕
17赞 Andrew Khosravian 2/24/2009 #5

如果您可以修改基类,则可以执行如下操作:

class Base
{
public:
// not implemented will cause a link error
    Base(const Derived &d);
    const Base &operator=(const Derived &rhs);
};

根据你的编译器,它应该为你提供翻译单元,也许还有切片发生的函数。

评论

0赞 bk1e 2/24/2009
更好的方法是将未实现的复制构造函数和赋值运算符设为私有。这假设你实际上也不需要。
1赞 j_random_hacker 2/24/2009
+1,非常好的解决方案。(我可以指出,这是所提出问题的第一个实际解决方案——所有其他“解决方案”都简单地解释了为什么将按值语义与继承结合起来是个坏主意。
2赞 Baiyan Huang 2/24/2009
是的,它类似于 Nick 的解决方案,它很酷,但它假设我们已经知道哪个类有这样的问题 - 这是不切实际的,因为我们的代码库中有这么多类。但谢谢,它确实有助于识别已知类的对象切片。
0赞 j_random_hacker 2/24/2009
@Baiyan:您可以通过 #defining 宏DISALLOW_SLICE(base,派生)来减少击键次数。此外,如果您的代码库格式一致,则可以编写一个脚本,在每个类的末尾之前插入此代码。这是一个黑客,但它可以为您节省一些时间。
2赞 L. F. 6/3/2019
在 C++ 11 中,引入了将其转换为编译器错误。= delete
4赞 CB Bailey 2/24/2009 #6

解决这个问题的最好方法通常是遵循Scott Meyer的建议(参见有效的C++),即只在继承树的叶节点上具有具体的类,并通过至少具有一个纯虚函数(析构函数,如果没有其他的话)来确保非叶类是抽象的。

令人惊讶的是,这种方法也经常以其他方式帮助澄清设计。在任何情况下,隔离一个通用抽象接口的努力通常都是值得的设计工作。

编辑

虽然我最初没有说清楚,但我的答案来自这样一个事实,即在编译时不可能准确地警告对象切片,因此,如果您启用了编译时断言或编译器警告,可能会导致错误的安全感。如果您需要了解对象切片的实例并需要更正它们,那么这意味着您有更改遗留代码的愿望和能力。如果是这样的话,那么我认为你应该认真考虑重构类层次结构,作为使代码更健壮的一种方式。

我的理由是这样的。

考虑一些定义类 Concrete1 并在此函数的推理中使用它的库代码。

void do_something( const Concrete1& c );

传递类型 be 引用是为了提高效率,一般来说,这是一个好主意。如果库将 Concrete1 视为值类型,则实现可能会决定创建输入参数的副本。

void do_something( const Concrete1& c )
{
    // ...
    some_storage.push_back( c );
    // ...
}

如果传递的引用的对象类型确实是某种派生类型,而不是其他派生类型,则此代码很好,不会执行切片。有关此函数调用的一般警告可能只产生误报,并且很可能无济于事。Concrete1push_back

考虑一些客户端代码,该代码派生自另一个函数并将其传递给另一个函数。Concrete2Concrete1

void do_something_else( const Concrete1& c );

由于该参数是通过引用获取的,因此此处不会对要检查的参数进行切片,因此在此处警告切片是不正确的,因为可能没有切片发生。将派生类型传递给采用引用或指针的函数是利用多态类型的一种常见且有用的方法,因此警告或禁止这样做似乎会适得其反。

那么哪里有错误呢?好吧,“错误”是传递对从类派生的东西的引用,然后被调用的函数将其视为值类型。

一般来说,没有办法针对对象切片生成一致有用的编译时警告,这就是为什么在可能的情况下,最好的防御措施是通过设计来消除问题。

评论

0赞 j_random_hacker 2/24/2009
你读过这个问题吗?他已经有一堆遗留代码,并希望找到切片发生的位置。你说的都是对设计新系统的人的好建议,但它对这个人没有帮助。
1赞 CB Bailey 2/24/2009
这是遗留代码,但他显然必须修改它。修改类层次结构绝对是尝试改进遗留代码时应考虑的操作。
0赞 j_random_hacker 2/24/2009
我不同意他“显然必须”修改它。也许在这个百万行代码库中只有 1 或 2 个错误需要修复。修改遗留代码中的类层次结构是您要考虑的最后手段,因为这是您可以做的最劳动密集型的事情之一。
0赞 CB Bailey 2/24/2009
好吧,我只是假设代码需要更改。如果它按原样工作,那么严格来说,如果发生切片,则无需担心。如果需要修复一两个错误,那么肯定是时候考虑一种风险最小的方法来修复它们了吗?
0赞 j_random_hacker 2/24/2009
当然,如果你有无限的时间和资源,那是正确的方法。但通常我们负担不起“最小风险”,我们只能承受“最小时间+风险”,时间权重很大。顺便说一句,完全重新设计一个大的阶级等级制度并不是一项没有风险的任务。
10赞 j_random_hacker 2/24/2009 #7

作为 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 */ }
};

尽管这减少了所需的工作量,但使用这种方法,您仍然需要弄乱每个基类以保护它。此外,如果从 到 雇用 的成员进行合法的转换,也会导致问题。(在这种情况下,可以使用更详细的解决方案。在任何情况下,一旦确定了对象切片的所有实例,就应删除此代码。BaseSomeOtherClassoperator Base()SomeOtherClassboost::disable_if<is_same<T, SomeOtherClass> >

致全世界的编译器实现者:测试对象切片绝对是值得的,可以创建(可选)警告!我想不出一个需要行为的例子,这在新手 C++ 代码中很常见。

[编辑 2015/3/27:]正如 Matt McNab 所指出的,您实际上不需要像上面那样显式声明复制构造函数和赋值运算符,因为它们仍将由编译器隐式声明。在 2003 年 C++ 标准中,12.8/2 下的脚注 106 明确提到了这一点:

由于模板构造函数从来都不是复制构造函数,因此此类模板的存在不会禁止复制构造函数的隐式声明。模板构造函数与其他构造函数(包括复制构造函数)一起参与重载解析,如果模板构造函数提供比其他构造函数更好的匹配,则可以使用模板构造函数来复制对象。

评论

0赞 Johannes Schaub - litb 2/24/2009
哈哈,我们的想法是一样的。我打算给他同样的东西:p结合 declspec deprecated / attribute deprecated,我认为这个会很震撼:p我也不会关心派生中的运算符 Base()。反正这太丑了:p但对你来说确实是+2
1赞 Johannes Schaub - litb 2/24/2009
但是,您可以将enable_if放在两者中,以便仅对派生类启用它们。因此,将不相关的类转换为 Base 仍然有效(我认为 boost::is_base_of)
1赞 j_random_hacker 2/24/2009
谢谢litb。虽然我的意思是在非派生类中允许合法的运算符 Base()。我认为您需要禁用派生类的转换,因为这些是您尝试检测的转换!
0赞 j_random_hacker 2/24/2009
哎呀,我明白了你想说的错路。我想我们说的是同样的事情——你建议只为派生类启用模板,我建议禁用合法的非派生类。:)
0赞 M.M 3/27/2015
如果您的代码以前使用隐式生成的代码工作,则无需为 Base 提供 copy-ctor 和 assignment-operator。模板版本不禁止隐式生成。
0赞 user52875 2/25/2009 #8

我稍微修改了你的代码:

class Base{
  public:
    Base() {}
    explicit Base(const Base &) {}
};

class Derived: public Base {};

void Func(Base)
{

}

//void Func(Derived)
//{
//
//}

//main
int main() {
  Func(Derived());
}

explicit 关键字将确保构造函数不会用作隐式转换运算符 - 当您想要使用它时,您必须显式调用它。

评论

0赞 j_random_hacker 2/26/2009
有趣的想法,但这也禁止在 MinGW (g++ 3.4.5) 和 Comeau 上使用“Func(Base());”。也就是说,您实际上也不能将 Base 对象作为值传递!OTOH,MSVC++9 允许 “Func(Base());” 和 “Func(Derived());”。想知道标准对此有何看法会很有趣。
0赞 user52875 2/27/2009
哦,看来我忽略了一个细节。我想它适用于仅通过引用传递的类。但是没有切片问题。另一方面,编译器会检测并抱怨它们是否按值传递,您将知道在哪里修复代码......
0赞 j_random_hacker 2/27/2009
实际上,C++的一个奇怪的规则似乎意味着,在某些情况下,即使对于通过引用传递的类,复制 ctor 也必须是可访问的。请看这里: gcc.gnu.org/bugs.html#cxx_rvalbind