英文:
Multi-level CRTP and Initialization
问题
#include <iostream>
#include <string>
#include <memory>
struct Val
{
std::string str;
void append(const std::string n)
{
str += n;
}
Val() : str("x")
{
}
};
template<typename T>
class base
{
public:
base(int n)
{
std::cout << "Base " << n << "\n";
// triggers error if not ptr
print();
}
void print()
{
static_cast<T*>(this)->impl_print();
}
~base()
{
std::cout << "DB\n";
}
};
template<typename T, typename V>
class implA : public base<implA<T, V>>
{
friend class base<implA<T, V>>;
public:
implA(int n) : base<implA<T, V>>(n)
{
std::cout << "implA " << n << "\n";
for (int i = 0; i < n; i++)
{
if (implVal == nullptr)
{
implVal = std::make_shared<V>();
}
implVal->append(std::to_string(n));
}
}
~implA()
{
std::cout << "iA\n";
}
protected:
std::shared_ptr<V> implVal = nullptr;
void impl_print()
{
if (implVal == nullptr)
{
implVal = std::make_shared<V>();
}
static_cast<T*>(this)->impl_print(*implVal);
}
};
class classX : public implA<classX, Val>
{
friend class implA<classX, Val>;
public:
classX(int n) : implA<classX, Val>(n)
{
std::cout << "classX " << n << "\n";
}
protected:
void impl_print(Val in)
{
std::cout << "p" << in.str << "\n";
}
};
int main()
{
classX x(2);
x.print();
return 0;
}
尝试进行多层继承,其中构造函数的参数会在构建过程中修改中间层的成员。遇到的问题是,在基类触发其 print 函数时,implVal 尚未分配,因此对其值的调用将导致运行时错误。
尝试通过将 implVal 更改为指针,并在需要时手动分配它来解决这个问题。这允许内部调用 print,但当 implClass 构造函数的主体解析时,implClass 再次为 null,就好像从未分配内存一样。这会导致尝试修改该值时出现空指针错误。发生了什么?
英文:
#include <iostream>
#include <string>
#include <memory>
struct Val
{
std::string str;
void append(const std::string n)
{
str += n;
}
Val() : str("x")
{
}
};
template<typename T>
class base
{
public:
base(int n)
{
std::cout << "Base " << n << "\n";
// triggers error if not ptr
print();
}
void print()
{
static_cast<T*>(this)->impl_print();
}
~base()
{
std::cout << "DB\n";
}
};
template<typename T, typename V>
class implA : public base<implA<T, V>>
{
friend class base<implA<T, V>>;
public:
implA(int n) : base<implA<T, V>>(n)
{
std::cout << "implA " << n << "\n";
for (int i = 0; i < n; i++)
{
implVal->append(std::to_string(n));
}
}
~implA()
{
std::cout << "iA\n";
}
protected:
std::shared_ptr<V> implVal = NULL;
void impl_print()
{
if (implVal == NULL)
{
implVal = std::make_shared<V>();
}
static_cast<T*>(this)->impl_print(*implVal);
}
};
class classX : public implA<classX, Val>
{
friend class implA<classX, Val>;
public:
classX(int n) : implA<classX, Val>(n)
{
std::cout << "classX " << n << "\n";
}
protected:
void impl_print(Val in)
{
std::cout<<"p"<< in.str << "\n";
}
};
int main()
{
classX x(2);
x.print();
return 0;
}
Trying to do some multi-level inheritance where the the arguments to a constructor modify the members of the mid-level during construction. Ran into the problem that implVal hasn't been allocated when base triggers its print, so calls to use its value will cause a runtime error.
Tried to fix that by changing implVal to a pointer and allocating it manually when needed. That allows the internal call to print to work, but when the body of implClass constructor resolves, implClass is null again, as if memory was never allocated. This causes a null ptr error when trying to modify the value. What is going on here?
答案1
得分: 1
base(int n)
{
std::cout << "Base " << n << "\n";
// 如果不是指针,会触发错误
print();
}
void print()
{
static_cast<T*>(this)->impl_print();
}
这种模式从根本上来说是不安全的。
直到你的构造函数返回之后,你的子类才会被构建,而且在C++中没有简单的方法在你的子类被构建之后运行代码。
修改一个未构建的对象的成员(或对象的未构建部分)是未定义行为。不要这样做。即使它能够工作,也不能保证在下次编译器检测到月相变化(或其他看似无关的原因)时仍然能够工作。
你能做的最好的事情就是采用后构造模式。最派生的类负责调用 post_construct
方法。
我不知道有什么干净的方法来强制要求你的派生类 T
调用 post-construct。
英文:
base(int n)
{
std::cout << "Base " << n << "\n";
// triggers error if not ptr
print();
}
void print()
{
static_cast<T*>(this)->impl_print();
}
this pattern is fundamentally not safe.
Your children are not constructed until after your constructor returns, and there is no simple way to run code after your children are constructed in C++.
Modifying members of a non-constructed object (or a non-constructed part of an object) is UB. Don't do it. Even if it works, it isn't guaranteed to work the next time your compiler detects a change in the moon phase (or other seemingly unrelated reasons).
The best you can do is have a post-construct pattern. The most-derived class is responsible to call a post_construct
method.
I am unaware of a way to cleanly enforce that your derived T
will call post-construct.
答案2
得分: 1
坚持使用发布的代码,你在这里违反了规则。当base
构造函数运行时,implA
尚不存在。因此,调用implA
的任何方法都是未定义行为,因此导致了崩溃。
CRTP(Curiously Recurring Template Pattern)不提供阻止你这样做的保护(而普通的具有virtual
函数的类出于非常好的原因可以提供保护)。我使用的解决方案是在调用对象的方法之前使用工厂函数构造对象。
英文:
Sticking with the code as posted, you're breaking the rules here. When the base
constructor runs, implA
doesn't exist yet. Calling any methods of implA
is therefore UB, hence your crash.
CRTP offers no protection to stop you from doing this (whereas regular classes with virtual
functions do, for very good reasons). The solution I use is to have a factory function that constructs the object before calling methods on it.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论