我在std :: thread周围有一个小的包装,如果可能,它会在生命周期尽头自动加入,看起来像这样(实际的包装具有更多功能,但该线程只是该问题的一个示例):>
#include <thread>
struct my_thread : std::thread
{
// Default Constructor
my_thread() = default;
// Forwards anything to thread
template<typename... Ts>
my_thread(Ts&&... ts)
:
std::thread( std::forward<Ts>(ts)... )
{}
// Destructor
~my_thread()
{
if ( joinable() ) { join(); }
}
};
然后我想添加一个重新启动成员函数,该成员函数连接当前线程(如果有的话)并创建一个新线程。我遇到的最简单的方法是:
// Restarts the thread
template<typename... Ts>
void restart(Ts&&... ts)
{
// Destruct current thread
this->~my_thread();
// Create new thread on this object
new (this) my_thread ( std::forward<Ts>(ts)... );
}
但是我不确定这是否是个好习惯,甚至可能失败。 我认为失败的原因是:
(1)如果调用析构函数时抛出该异常,则在取消堆栈堆栈时,将在销毁局部变量时再次调用该析构函数。
(2)如果调用构造函数时抛出任何异常,则该对象将处于未完成状态,并且当局部变量被破坏时,将在一个未完成的对象上调用该析构函数,这是未定义的行为
由于这两个问题都涉及异常,所以我认为我可以添加一条try / catch all语句,但是我不确定在失败时该怎么做。
例如,如果析构函数成功但构造函数失败,我该怎么办?
我不能让对象处于不完整的状态,因为当调用局部变量的析构函数时,它将是未定义的行为,因此我想到了只做无限循环,直到构造函数成功,但这听起来并不合理。就像一个好主意。
我应该将当前对象保存在某个地方,如果其中任何一个失败,则像这样还原吗?
// Restarts the thread
template<typename... Ts>
void restart(Ts&&... ts)
{
// Copy
char backup[ sizeof( my_thread ) ];
memcpy(backup, this, sizeof(my_thread) );
try
{
// Destruct current thread
this->~my_thread();
// Create new thread on this object
new (this) my_thread ( std::forward<Ts>(ts)... );
}
catch (...)
{
// Copy backup
memcpy(this, backup, sizeof(my_thread));
}
}
这感觉像一个可怕的主意,因为我不仅在没有例外的情况下创建不必要的副本,而且还忽略了构造函数/析构函数的任何副作用,因此我认为这不是一个好答案。
我还考虑过像这样使用std::thread
的move构造函数:
// Restarts the thread
template<typename... Ts>
void restart(Ts&&... ts)
{
static_cast<std::thread&>(*this) = std::move( std::thread{ std::forward<Ts>(ts)... } );
}
这似乎可行,但是我不确定这是一个陷阱。而且我认为一般来说,顶层代码更有用,因为它也适用于不可移动或不可复制的类型,所以我想知道最好的方法是什么。
具体来说,我的问题是“重构”这样的对象是否是一个好主意,并且在执行此操作时是否应考虑任何事项?
这也可以应用于任何事物,我只是使用线程,因为这是问题首先出现的地方。
这是我希望最终产品执行的操作的一个示例,但是,如果可能的话,请不要停止从std :: thread(或任何类型)继承:
struct my_thread
{
// The thread memory
char thread_mem[ sizeof(std::thread) ];
bool thread_constructed = false;
// Default Constructor
my_thread() = default;
// Forwards anything to thread
template<typename... Ts>
my_thread(Ts&&... ts)
{
new ( thread_mem ) std::thread ( std::forward<Ts>(ts)... );
thread_constructed = true;
}
// Destructor
~my_thread()
{
// Call destructor if it's constructed
if ( thread_constructed )
{
if ( reinterpret_cast<std::thread*>(thread_mem)->joinable() )
{
reinterpret_cast<std::thread*>(thread_mem)->join();
}
thread_constructed = false;
reinterpret_cast<std::thread*>(thread_mem)->~thread();
}
}
// Restarts the thread
template<typename... Ts>
void restart(Ts&&... ts)
{
// Destruct current thread
this->~my_thread();
// Create new thread on this object
new (this) my_thread ( std::forward<Ts>(ts)... );
}
};
我希望有可能使100%的泛型类型能够处理此问题,我相信双重析构函数会从以前调用问题以及被破坏的不完整对象:
template<typename T>
struct restartable_type
{
// Memory
alignas(T) char mem[ sizeof(T) ];
bool constructed = false;
template<typename... Ts>
restartable_type(Ts&&... ts)
{
new ( mem ) T ( std::forward<Ts>(ts)... );
constructed = true;
}
~restartable_type()
{
reinterpret_cast<T*>(mem)->~T();
constructed = false;
}
template<typename... Ts>
void restart(Ts&&... ts)
{
this->~restartable_type();
new ( this ) restartable_type<T> ( std::forward<Ts>(ts)... );
}
};
我要改进的唯一一件事就是从T继承,这样我就可以在restartable_type上从T调用成员函数,但是我不知道有什么方法可以解决此析构函数问题,我能想到的就是重载operator->
来重定向到T,但这只是解决方案的一半,因为我更喜欢使用。 (点),我只是不知道如何解决析构函数的问题,这就是我想要的帮助。如果我不清楚这个问题,我深表歉意。
答案 0 :(得分:1)
如果您的主构造函数抛出以下错误,而您可以退回到noexcept
默认构造函数,则可以使此想法起作用:{p}
template<typename... Ts>
void restart(Ts&&... ts)
{
this->~my_thread();
try {
new (this) my_thread(std::forward<Ts>(ts)...);
} catch (...) {
new (this) my_thread();
throw;
}
}
您甚至可以通过检查is_nothrow_default_constructible<std::thread>::value
来启用它,这是正确的。
但是,在执行此操作时可能需要考虑太多。例如,没有人可以进一步继承您的课程。或者,您可能隐式地公开了一些接口的方法,可以在重建时打破类协定。使用std::thread
时,它们分别是get_id()
和native_handle()
-但是,它们在分配时也被破坏了,因此,如果您的用户知道 {{1 }}是一项作业。更糟糕的是,当用户先呼叫restart()
然后呼叫detach()
时会产生歧义。它会起作用,但在用户可能期望的意义上来说并非如此。
还有另一件事要考虑:您不会通过加入线程来停止线程。加入只是等待线程完成。您需要一些其他的信号通知机制来告知线程完成,然后使用这种包装程序并不会带来什么好处。
答案 1 :(得分:0)
有一些通用的解决方法,每种方法都有其起伏:
std::optional<T>
通过拥有std::optional<T>
的成员,您可以像这样围绕T
提供一个瘦的拥有包装器:
#include <optional>
template<typename T>
struct reset_type
{
// The value itself
std::optional<T> value;
// Constructs value in-place with arguments
template<typename... Ts>
constexpr reset_type(Ts&&... ts)
:
value(std::in_place_t{}, std::forward<Ts>(ts)... )
{}
// Resets the type
template<typename... Ts>
constexpr void reset(Ts&&... ts)
{
value.reset();
value.emplace( std::forward<Ts>(ts)... );
}
// Conversion operator
constexpr operator T&() { return *value; }
constexpr operator const T&() const { return *value; }
// Accesses T
constexpr T* operator->() { return &(*value); }
constexpr const T* operator->() const { return &(*value); }
};
这种方法很可能是实现此包装的最简单,最好的方法,您唯一要放弃的就是通过来访问成员。 (点)运算符。
我们可以从T继承并以这种方式构造它:
template<typename T>
struct reset_type : T
{
// Constructs value in-place with arguments
template<typename... Ts>
constexpr reset_type(Ts&&... ts)
:
T( std::forward<Ts>(ts)... )
{}
// Resets the type
template<typename... Ts>
constexpr void reset(Ts&&... ts)
{
this->~reset_type();
new ( this ) reset_type<T> ( std::forward<Ts>(ts)... );
}
};
这个问题出在构造函数或析构函数抛出异常时,我不确定它的细节是什么,但是除非它们抛出异常,否则不会发生任何坏事
这样做的好处是您可以使用静态强制转换将其转换为T,因为它继承自它,并且可以使用来访问T。 (点)运算符,使它变得更加有用。
用T
替换几个std::remove_cv<T>
也是值得的。
如果还有其他解决方案,请告诉我,我将在此处添加。