【问题标题】:How to generate a compiler warning/error when object sliced对象切片时如何生成编译器警告/错误
【发布时间】:2010-10-09 11:38:40
【问题描述】:

我想知道是否可以让编译器针对以下代码发出警告/错误:

注意:

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 提交反馈,希望他们能尽快修复:

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=421579

-白燕

【问题讨论】:

    标签: c++ compiler-construction truncate slice


    【解决方案1】:

    这通常被称为Object Slicing,并且是一个众所周知的问题,有自己的维基百科文章(尽管它只是对该问题的简短描述)。

    我相信我使用的编译器有一个警告,您可以启用它来检测和警告这一点。但是,我不记得是哪一个了。

    【讨论】:

      【解决方案2】:

      不是真正解决您当前问题的方法,但是....

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

      如果你总是这样做,对象切片永远不会成为问题(引用不会被切片!)。

      【讨论】:

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

      您是说 Derived 是一个基数,因此它应该适用于任何需要基数的函数。如果这是一个真正的问题,那么继承可能不是您真正想要使用的。

      【讨论】:

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

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

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

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

      这适用于我的测试(编译时警告)。请注意,它不能解决复制情况,但类似构造的赋值运算符应该可以解决问题。

      希望这会有所帮助。 :)

      【讨论】:

      • 谢谢尼克,这真的很酷!但在我的情况下,我什至不知道哪些类有这样的问题——我需要编译器来帮助我找到这样的情况,在大量的代码中。 -白燕
      【解决方案5】:

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

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

      取决于您的编译器应该为您提供翻译单元,以及可能发生切片的函数。

      【讨论】:

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

      解决此问题的最佳方法通常是遵循 Scott Meyer 的建议(请参阅 Effective C++),即在继承树的叶节点处仅具有具体类并确保非叶类是通过至少有一个纯虚函数(析构函数,如果没有别的)来抽象。

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

      编辑

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

      我的理由是这样的。

      考虑一些定义类 Concrete1 并在该函数的接口中使用它的库代码。

      void do_something( const Concrete1& c );
      

      传递类型是参考是为了提高效率,通常是一个好主意。如果库认为 Concrete1 是值类型,则实现可能会决定复制输入参数。

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

      如果传递的引用的对象类型确实是Concrete1 而不是其他派生类型,那么这段代码没问题,不执行切片。对此push_back 函数调用的一般警告可能只会产生误报,而且很可能没有帮助。

      考虑一些从Concrete1 派生Concrete2 并将其传递给另一个函数的客户端代码。

      void do_something_else( const Concrete1& c );
      

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

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

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

      【讨论】:

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

      作为Andrew Khosravian's answer 的变体,我建议使用模板复制构造函数和赋值运算符。这样您就不需要知道给定基类的所有派生类来保护该基类不被切片:

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

      虽然这减少了所需的工作量,但使用这种方法,您仍然需要处理每个基类以保护它。此外,如果从BaseSomeOtherClass 的合法转换使用operator Base()SomeOtherClass 成员,则会导致问题。 (在这种情况下,可以使用涉及boost::disable_if&lt;is_same&lt;T, SomeOtherClass&gt; &gt; 的更复杂的解决方案。)在任何情况下,一旦确定了对象切片的所有实例,就应该删除此代码。

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

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

      因为模板构造函数永远不是复制构造函数,所以这样的模板的存在不会抑制复制构造函数的隐式声明。模板构造函数与其他构造函数(包括复制构造函数)一起参与重载决议,如果模板构造函数提供比其他构造函数更好的匹配,则可以使用模板构造函数来复制对象。

      【讨论】:

      • 哈哈,我们的想法差不多。我打算给他同样的东西:p 结合 declspec deprecated / attribute deprecated,我认为这个会摇滚:p 我也不会关心派生中的 operator Base()。无论如何,这太丑陋了:p但确实为你+2
      • 您可以将 enable_if 放在两者中,以便它们仅对派生类启用。所以一个不相关的类到 Base 的转换仍然有效(我认为 boost::is_base_of)
      • 感谢 litb。尽管我的意思是在非派生类中允许合法的运算符 Base() 。我认为您需要禁用派生类的转换,因为这些是您要检测的转换!
      • 哎呀,我把你想说的话弄错了。我认为我们在说同样的事情——你建议只为派生类启用模板,我建议禁用合法的非派生类。 :)
      • 如果您的代码以前使用隐式生成的代码工作,则不需要为 Base 提供复制操作符和赋值操作符。模板版本不禁止隐式生成。
      【解决方案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 关键字将确保构造函数不会被用作隐式转换运算符 - 当你想使用它时必须显式调用它。

      【讨论】:

      • 有趣的想法,但这也禁止“Func(Base());”在 MinGW (g++ 3.4.5) 和 Comeau 上。也就是说,您实际上也不能将 Base 对象作为值传递! OTOH,MSVC++9 允许“Func(Base());”和“Func(Derived());”。知道标准对此有何评论会很有趣。
      • D'oh,看来我忽略了一个细节。我想它适用于仅通过引用传递的类。但是没有切片问题。另一方面,编译器会检测并抱怨它们是否是按值传递的,并且您会知道在哪里修复代码...
      • 实际上,这似乎是 C++ 的一个奇怪规则,这意味着在某些情况下,即使对于通过引用传递的类,复制 ctor 也必须是可访问的。见这里:gcc.gnu.org/bugs.html#cxx_rvalbind
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-16
      • 2010-11-28
      • 1970-01-01
      相关资源
      最近更新 更多