18

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

笔记:

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

-白燕

4

8 回答 8

16

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

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

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

于 2009-02-24T05:09:03.167 回答
9

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

尽管这减少了所需的工作量,但使用这种方法,您仍然需要弄乱每个基类以保护它。此外,如果从BaseSomeOtherClass雇用operator Base(). SomeOtherClass(在这种情况下,可以使用更复杂的解决方案boost::disable_if<is_same<T, SomeOtherClass> >。)在任何情况下,一旦确定了对象切片的所有实例,就应该删除此代码。

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

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

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

于 2009-02-24T11:20:18.190 回答
4

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

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

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

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

希望这可以帮助。:)

于 2009-02-24T04:52:13.580 回答
2

解决这个问题的最好方法通常是遵循 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函数调用的一般警告可能只会产生误报,并且很可能没有帮助。

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

void do_something_else( const Concrete1& c );

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

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

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

于 2009-02-24T09:35:56.300 回答
1

这通常被称为对象切片,并且是一个众所周知的问题,有自己的 Wikipedia 文章(尽管它只是对该问题的简短描述)。

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

于 2009-02-24T03:44:16.847 回答
1

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

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

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

于 2009-02-24T03:54:09.883 回答
1
class Derived: public Base{};

你说 Derived 是一个基础,所以它应该在任何需要基础的函数中工作。如果这是一个真正的问题,那么继承可能不是您真正想要使用的。

于 2009-02-24T04:01:38.290 回答
0

我稍微修改了你的代码:

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

class Derived: public Base {};

void Func(Base)
{

}

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

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

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

于 2009-02-24T19:46:41.857 回答