c++ - 在新版本的 gcc 上返回隐式不可复制结构的 std::map 时出现编译错误

标签 c++ c++11 gcc

我在新版本的 gcc (4.9+) 上遇到了这个奇怪的编译错误。

代码如下:

#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <map>

using namespace std;

struct ptrwrap
{
    unique_ptr<int> foo;
};

template <typename T>
struct holder
{
    holder() = default;

    holder(const holder& b)
        : t(b.t)
    {
    }

    holder(holder&& b)
        : t(std::move(b.t))
    {
    }

    holder& operator=(const holder& h)
    {
        t = h.t;
        return *this;
    }

    holder& operator=(holder&& h)
    {
        t = std::move(h.t);
        return *this;
    }

    T t;
};

struct y_u_no_elision
{
    holder<ptrwrap> elem;
};

typedef map<std::string, y_u_no_elision> mymap;

mymap foo();

int main()
{
    auto m = foo();
    m = foo();
    return 0;
}

Here它在 ideone 上也有实际错误。基本上它归结为使用 ptrwrap 的删除复制构造函数。哪个……不应该发生。 map 按值返回(即移动),因此不能存在拷贝。

现在相同的代码在旧版本的 gcc(我试过 4.2 和 4.3)、我试过的所有 clang 版本以及 Visual Studio 2015 上都没有问题。

奇怪的是,如果我删除 holder 模板的显式复制和移动构造函数,它也会在 gcc 4.9+ 上编译。如果我将 map 更改为 vectorunordered_map 它也可以正常编译( here 是代码编译版本的链接使用 unordered_map)

所以...这是 gcc 4.9 错误还是其他编译器允许我看不到的东西?我能做些什么而不涉及更改 holder 类吗?

最佳答案

简短回答:这是 libstdc++ 中的错误。根据标准中 [container.requirements.general] 中的分配器感知容器需求表(自 C++11 以来未更改),容器移动分配:

Requires: If allocator_traits<allocator_type>::propagate_on_container_move_assignment::value is false, T is MoveInsertable into X and MoveAssignable. [...]

(X 是容器类型,T 是它的 value_type )

您正在使用默认分配器,它有 using propagate_on_container_move_assignment = true_type; ,所以上述要求不适用; value_type 应该没有特殊要求.

快速修复:如果您无法触摸 holder ,一种解决方案是更改 y_u_no_elision , 添加

y_u_no_elision(const y_u_no_elision&) = delete;
y_u_no_elision(y_u_no_elision&&) = default;

长话短说:该错误主要是由 this line in stl_tree.h 引起的.

_Rb_treestd::map 的底层实现并且其移动赋值运算符定义中的那一行基本上执行上面标准引用指定的检查。但是,它使用简单的 if 来实现,这意味着即使满足条件,另一个分支也必须编译,即使它不会在运行时执行。缺少 Shiny 的新 C++17 if constexpr ,这应该使用诸如标签分派(dispatch)之类的东西来实现(对于前两个条件 - 第三个是真正的运行时检查),以避免在所采用的分支之外实例化代码。

然后错误是由this line引起的,它使用 std::move_if_noexceptvalue_type .说来话长。

value_typestd::pair<const std::string, y_u_no_elision> .

在您的初始代码中:

  • holder具有非删除、非 noexcept 复制和移动构造函数。
  • 这意味着 y_u_no_elision 的相应隐式声明的构造函数也将是非删除和非 noexcept。
  • 这些特征传播给 value_type 的构造函数.
  • 结果为 std::move_if_noexcept返回 const value_type&而不是 value_type&& (如果可以,它会回退到复制 - 请参阅 these docs)。
  • 这最终会导致 y_u_no_elision 的复制构造函数被调用,这将导致 holder<ptrwrap>要实例化的复制构造函数定义,它试图复制 std::unique_ptr .

现在,如果您从 holder 中删除用户声明的拷贝并移动构造函数和赋值运算符 :

  • holder将得到隐式声明的。复制构造函数将被删除,移动构造函数将被默认,不删除和noexcept .
  • 这会传播到 value_type , 除了一个与 holder 无关的异常(exception): value_type的移动构造函数将尝试从 const std::string 移动;这不会调用 string的移动构造函数(在本例中为 noexcept),而是它的复制构造函数,如 string&&无法绑定(bind)到 const string 类型的右值.
  • string的复制构造函数不是 noexcept (它可能必须分配内存),所以 value_type的移动构造函数也不会。
  • 那么,为什么代码可以编译?因为std::move_if_noexcept背后的逻辑: 它返回一个右值引用,即使参数的移动构造函数不是 noexcept ,只要参数不是可复制构造的(如果不能回退到复制,它会回退到非 noexcept 移动);和 value_type不是,因为 holder's删除复制构造函数。

这是上面快速修复背后的逻辑:你必须做一些事情才能使 value_type有一个有效的移动构造函数和一个删除的复制构造函数,以便从 move_if_noexcept 获得右值引用.这是因为您将无法制作 value_type有一个noexcept由于 const std::string 移动构造函数,如上所述。

关于c++ - 在新版本的 gcc 上返回隐式不可复制结构的 std::map 时出现编译错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39040609/

相关文章:

c++ - libboost_log_setup.a 库的原因?

c++ - 为什么树向量化会使这种排序算法慢2倍?

c++ - 一个 friend 的缩写模板函数——clang和gcc的区别

c++ - 将 std::vector<T> move 到 T*

c++ - 尝试在 vector 中查找 unique_ptr 时出现编译错误

python - 通过引用返回 vector<string>

c++ - 在 VC++ 中创建没有 "new"的动态数组

c++ - 递归:基本情况与小版本

c++ - 如何以原子方式否定 std::atomic_bool?

C++11 static assert for equality comparable type?