【问题标题】:CRTP base constructor crashes because child is not constructedCRTP 基本构造函数崩溃,因为未构造子级
【发布时间】:2018-11-30 15:36:57
【问题描述】:

我有自动生成的类,但我想让最终用户添加自定义成员函数和构造函数。

我的方法是使用没有成员变量只有函数的 CRTP 基类。

问题在于构造函数。如果我在我的 CRTP 中定义了一个构造函数,我将无法正确访问子类,因为它尚未构造,因为子类构造函数仅在构造 CRTP 基础之后才被调用。

#include <iostream>
#include <string>

template<class Child>
struct Foo {
  Foo(std::string i) {
    // Childs constructor is not run yet.
    std::cout << static_cast<Child&>(*this).d.size(); // Prints trash
    static_cast<Child&>(*this).d = i; // Segfault here
    (void) i;
  }
};

// Cannot change this class.
struct Bar : Foo<Bar> {
  using base_t = Foo<Bar>;
  using base_t::base_t;
  std::string d;
};


int main()
{
  Bar bar("asdgasdgsag");
  std::cout << "bar.d: " << bar.d << std::endl;
}

有没有办法解决这个问题?

【问题讨论】:

  • @FrançoisAndrieux:是的,到目前为止很清楚。
  • 这是一个众所周知的问题。虚函数和继承也会有同样的问题。无法解决,构造函数不能使用派生类的东西。
  • 您需要两阶段初始化。先构造,然后在单独的初始化函数中完成。
  • 问题解决了!第二阶段可以通过使用默认参数临时绑定到构造函数参数来完成!
  • 如果您发现自己编写了很多具有“相同”成员的类,请考虑将该成员移至它们的公共基类。在您的情况下,Foo 的所有孩子都应该有 std::string d; 成员。将其移至Foo 是自然而恰当的。

标签: c++ c++11 constructor crtp


【解决方案1】:

你的基础构造函数不能对子类做任何事情。完全没有。后者尚未构建。

CRTP 允许Foo 的其他成员函数这样做,但仅此而已。

在您提出的设计中没有“快速修复”。您可以向Foo 添加类似Init 的函数以便稍后执行此操作(并从孩子的ctor 调用它),或者(理想情况下)重新考虑您的方法。

有点奇怪,您不能修改子类,而是向Base 添加东西——这不是继承应该完成的方式,而且您似乎正在尝试使用 CRTP 来破解那个,但已经发现为什么这不是一个有效的黑客攻击。

在不知道您要完成什么的情况下,我无法比这更精确。

也许工厂函数可以帮助你?或者从Bar继承。

【讨论】:

  • 我有自动生成的类,因此我可以通过更改生成器来添加 CRTP 基础。然后,最终用户可以提供一个 CRTP 基础并定义一些类型特征专业化,以将 CRTP 基础与其所需的类型正确关联。我不能反过来使用继承,因为我可能会得到一个std::vector&lt;Bar&gt;,我想将其元素视为Foo
  • @AndreasPasternak 如果您可以以这种方式更改生成器,那么您当然可以用另一种方式更改它:) 无论Foo&lt;Bar&gt; ctor 对Bar 子对象所做的事情都应该在Bar 中相反,时期!
【解决方案2】:

有一个解决方案。它不是 C++17 标准的 UB,而是 C++14 的 UB。也不干净。

#include <iostream>
#include <string>

#if __cpp_inheriting_constructors < 201511
  #error "Not supported."
#endif

template<class Child>
struct post_construct{
    Child* constructed;
    std::string i;
    ~post_construct(){
        constructed->d=std::move(i);
    }
};


template<class Child>
struct Foo {
  Foo(std::string i,post_construct<Child>&& m=post_construct<Child>{}) {
    m.constructed = static_cast<Child*>(this);
    m.i = std::move(i);
  }
};

// Cannot change this class.
struct Bar : Foo<Bar> {
  using base_t = Foo<Bar>;
  using base_t::base_t;
  std::string d;
};


int main()
{
  Bar bar("asdgasdgsag"); //temporary materialization here
                          //then temporary destroyed at end of full-expression
  std::cout << "bar.d: " << bar.d << std::endl;
}

Demo

Assembly code and compiler checks

【讨论】:

  • 在构造函数上使用“隐藏”参数是非常混乱和不灵活的。不过很聪明。
  • 这仍然是 UB。我看不出它如何解决任何问题。 Child 只有在它的构造函数终止后才被构造。
  • @SergeyA 这是c++14 中的UB。不在c++17。上次我们意见分歧时,原因是标准的语义变化。
  • @SergeyA 这个技巧依赖于基础 ctor 的默认参数的顺序和生命周期,该参数延伸到派生子对象迅速增长的生命周期。就我个人而言,我不相信链接的标准段落是相关的,但我知道什么
  • @LightnessRacesinOrbit 这是P01361R1 在标准中引入的。 MSVC 尚不支持,但 gcc 和 clang 支持。如果你看一下这个程序集here,对post_construct 的析构函数的调用是在调用derived_member 的默认构造函数之后执行的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-25
相关资源
最近更新 更多