C++ 在异常中不嵌入std::string的规则是否仍然适用于移动构造函数?

C++ 在异常中不嵌入std::string的规则是否仍然适用于移动构造函数?,c++,exception,c++11,move-constructor,C++,Exception,C++11,Move Constructor,我不久前听说我不应该创建异常类,这些异常类的字段为std::string类型。就是这样。基本原理是,如果内存分配失败,std::stringcopy构造函数可以引发异常,如果在捕获当前处理的异常之前引发异常,则程序将终止 然而,在移动构造函数的世界中,它仍然有效吗?在抛出异常时,不会使用move构造函数而不是copy构造函数吗?我是否正确理解,使用C++11不会发生内存分配,不存在异常的可能性,并且现在异常类中的std::string绝对没有问题?引发异常时是否复制字符串取决于catch块 举个

我不久前听说我不应该创建异常类,这些异常类的字段为
std::string
类型。就是这样。基本原理是,如果内存分配失败,
std::string
copy构造函数可以引发异常,如果在捕获当前处理的异常之前引发异常,则程序将终止


然而,在移动构造函数的世界中,它仍然有效吗?在抛出异常时,不会使用move构造函数而不是copy构造函数吗?我是否正确理解,使用C++11不会发生内存分配,不存在异常的可能性,并且现在异常类中的
std::string
绝对没有问题?

引发异常时是否复制
字符串取决于
catch

举个例子:

#include <iostream>

struct A
{
};

void foo()
{
   throw A();
}

void test1()
{
   try
   {
      foo();
   }
   catch (A&& a)
   {
   }
}

void test2()
{
   try
   {
      foo();
   }
   catch (A const& a)
   {
   }
}

void test3()
{
   try
   {
      foo();
   }
   catch (A a)
   {
   }
}

int main()
{
   test1();
   test2();
   test3();
}
#包括
结构A
{
};
void foo()
{
抛出一个();
}
void test1()
{
尝试
{
foo();
}
捕获(A和A)
{
}
}
void test2()
{
尝试
{
foo();
}
捕获(常量和A)
{
}
}
void test3()
{
尝试
{
foo();
}
捕获(A)
{
}
}
int main()
{
test1();
test2();
test3();
}
您不会在
test1
test2
中复制
a
,但最终会在
test3
中复制。答案是:

是的,您仍然不想在异常类型中嵌入
std::string
。异常经常被复制,有时您不知道。例如,在某些平台上,
std::rethrow_exception
将复制异常(在某些平台上则不会)

为了获得最佳实践,请保留副本构造函数
noexcept

然而,一切都没有失去。一个鲜为人知的事实是,C++在标准中始终具有不可变的REF计数字符串类型(具有非投掷复制构造函数),只是带有混淆的名称。实际上有两个名字:

logic_error
runtime_error
这些类型的规范必须包含不可变的ref计数字符串类对象。嗯,不是一成不变的。可以用赋值替换字符串。但是您不能在其他地方修改字符串

我的建议是从这些类型中的一种派生,或者如果不可接受,则嵌入其中一种类型,并将其视为不可变的引用计数字符串类型:

#include <stdexcept>
#include <iostream>

class error1
    : public std::runtime_error
{
    using msg_ = std::runtime_error;
public:
    explicit error1(std::string const& msg)
        : msg_(msg)
    {}
};

class error2
{
    std::runtime_error msg_;
public:
    explicit error2(std::string const& msg)
        : msg_(msg)
    {}

    char const* what() const noexcept {return msg_.what();}
};

void
test_error1()
{
    try
    {
        throw error1("test1");
    }
    catch (error1 const& e)
    {
        std::cout << e.what() << '\n';
    }
}

void
test_error2()
{
    try
    {
        throw error2("test2");
    }
    catch (error2 const& e)
    {
        std::cout << e.what() << '\n';
    }
}

int
main()
{
    test_error1();
    test_error2();
}

(重载#8)实际上是
noexcept
,因此该部分似乎是合理的。我不知道在哪里确认投掷确实移动,也不知道这是否是唯一的理由反对<代码> STD::String 异常。如果你的程序非常关键,它必须用零可用堆内存操作,也许你应该考虑用严格的内存需求重写它,也许是用不同的语言,或者使用不同的库。否则,让它终止,在这一点上它是没有用的。我想它仍然会在
test3
中移动,因为
A()
是一个右值。如果是
A;扔一个
foo
内部,它应该是在
test3
第15.1/3节中复制的“抛出异常副本初始化一个临时对象,称为异常对象”。§15.1/5“当抛出的对象是类对象时,即使省略了复制/移动操作,为复制初始化选择的构造函数和析构函数也应可访问“.Ergo,
抛出
也会导致复制/移动,尽管它可能会被省略。使用
-std=c++1z
的铿锵SVN允许在
静态断言中删除那些讨厌的引号
static_assert(std::is_nothrow_copy_constructible<error1>{}, "");
static_assert(std::is_nothrow_copy_assignable   <error1>{}, "");
static_assert(std::is_nothrow_copy_constructible<error2>{}, "");
static_assert(std::is_nothrow_copy_assignable   <error2>{}, "");