c ++ CRTP堆栈损坏

时间:2014-06-04 22:58:09

标签: c++ crtp stack-corruption

在MSVC 2013上运行以下代码时,x64 Debug config,当退出main()函数时,它将显示一个带有此着名错误消息的消息框

"Run-Time Check Failure #2 - Stack around the variable 'tmp' was corrupted.". 

我无法回答的问题是:为什么?

请注意,在Release配置上运行时不会出现任何错误消息。 (为什么?)

免责声明:这只是一个示例代码,这意味着我正在尝试在其他类(一个基础和几个派生)上使用相同的设计,使用更多的方法和模板参数,并使用比基本的int *。

更复杂的数据类型
#include <iostream>

template <class T>
class base {
public:
    base() {
        static_cast<T*>(this)->setData();
    }
    ~base() {
        static_cast<T*>(this)->destroyData();
    }

    void print() {
        static_cast<T*>(this)->print_int();
    }
};

class derived : public base<derived> {
public:

    void setData() {
        x = new int();
    }

    void destroyData() {
        delete x;
        x = nullptr;
    }

    void print_int() {
        std::cout << "x = " << *x << std::endl;
    }

private:

    derived() {}
    derived(const derived& other) {}
    inline derived& operator= (derived copy) {}

    int *x;
};

int main() {
    base<derived> tmp;

    tmp.print();

    return 0;
}

修改 @Yakk如果我理解正确,你建议解决这个代码:

#include <iostream>

template <class T>
class mix_in : public T
{
public:
    mix_in() { (this)->setData(); }
    ~mix_in() { (this)->destroyData(); }
    void print() { (this)->print_int(); }
};

class foo
{
public:

    void setData()
    {
        x = new int();
    }

    void destroyData()
    {
        delete x;
        x = nullptr;
    }

    void print_int()
    {
        std::cout << "x = " << *x << std::endl;
    }

    foo() {}

private:

    int *x;
};

int main()
{
    mix_in<foo> tmp;

    tmp.print();

    return 0;
}

工作得很好,谢谢。我可能会使用这种模式,因为似乎没有解决方案将CRTP用于我正在尝试做的事情。

但我仍然想了解堆栈损坏发生的原因。尽管围绕使用的所有讨论都是使用CRTP进行所有事情,但我想非常清楚地理解它为什么会发生。

再次感谢你。

4 个答案:

答案 0 :(得分:6)

至于main()中显示的代码:

int main() {
    base<derived> tmp;

    tmp.print();

    return 0;
}

base<derived> tmp;错误使用此模式,您需要derived tmp;

CRTP的要点是派生类注入到基类,并为在编译时解析的某些功能提供实现。
基类本质上是预期的,只能通过继承 实例化 。因此,您应该确保在继承上下文之外不要有任何实例化。

为了避免被这种误解困扰的实施用户,请使base的构造函数受到保护:

template <class T>
class base {
public:
    ~base() {
        static_cast<T*>(this)->destroyData();
    }

// ...
protected:
    T* thisPtr;
    base() : thisPtr(static_cast<T*>(this)) {
    }
};

注意:基类不应该调用依赖于基类构造函数中完全初始化的derived类方法的任何方法,正如您在示例中所示(static_cast<T*>(this)->setData();)!

您可以将参考文献存储到T*以供日后使用,如上例所示。


class derived : public base<derived> {
public:
    derived() {
        setData();
    }

    void destroyData() {
        delete x;
        x = nullptr;
    }

    void print_int() {
        std::cout << "x = " << *x << std::endl;
    }

private: // ????
   derived(const derived& other) {} 
   inline derived& operator= (derived copy) {}

   int *x;
};

还有点不清楚,为什么要为你的班级隐藏复制构造函数和赋值运算符?

答案 1 :(得分:4)

tmpbase<derived>,而不是derived。 CRTP的重点是基类&#34;知道&#34;对象的实际类型,因为它作为模板参数传递,但如果手动创建base<derived>,则基类函数会认为对象是derived,但它不是 - 它只是base<derived>。结果发生了奇怪的事情(未定义的行为)。 (如在另一个答案中所述,你也在不设置它的情况下打印出来的东西......)

关于你的第二个问题,MSVC为检测那些程序员错误而产生的检查似乎在发布模式下被关闭,可能是出于性能原因。

答案 2 :(得分:4)

template <class T> class mix_in:public T {
  public:
    base() { (this)->setData(); }
    ~base() { (this)->destroyData(); }
    void print() { (this)->print_int(); }
};

derived重命名为foo。不要继承mix_in中的basefoo

base中交换mix_in main

CRTP不是所有问题的解决方案。

答案 3 :(得分:2)

你在评论中说,

  

派生类永远不会被实例化,它的构造函数永远不会被调用

其他人已经指出了未构造对象的未定义使用。具体而言,请考虑int *xderived成员,并且您的base<derived>用法会按顺序生成对derived::set_data()derived::print_int()和退出的调用derived::destroy_data(),一个不存在的对象的所有成员函数,指的是您从未分配空间的成员 1

但我认为你所追求的是完全合法的。保理代码是模板和继承的全部要点,即通过抽象出样板来使重要代码更容易识别,更容易理解,以及任何后续的重构更容易。

所以:

template < class T >
struct preconstruction_access_subset {};  // often left empty

template < class T, class S = preconstruction_access_subset<T> >
struct base : S {
        base() { S::setData(); }
       ~base() { S::destroyData(); }
        void print() { S::print_int(); }
};

// ... later ...

#include <iostream>

struct derived;
template<> struct preconstruction_access_subset<derived> {
        // everything structors in `base<derived>` need
        int *x;
        void setData()     { x = new int; }
        void destroyData() { delete x; }
        void print_int()   { std::cout << x << '\n'; }
};
struct derived : base<derived> {
        // everything no structors in any base class need to access
};

int main() {
        base<derived> tmp;
        tmp.print();
}

在这个示例中,print_int()在构造期间实际上并未被访问,因此您可以将其放回derived类并从`base访问它,就像“普通”CRTP一样,这只是一个问题对你的实际代码来说最清楚和最好的。


1 这不是唯一的问题,只是唯一一个没有考虑编译器本身可能需要放置在构造序列上的依赖的问题。在所有可能的模板/继承/成员函数/等的{de,con} struction和编译器代码因子分析期间,考虑独立名称绑定和潜在内联以及对象标识变形的方式。组合。不要只是提升int *x;以解决当前的症状,即使你的事情还没有那么远,这也会给你带来更多麻烦。 < / p>