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

Compilation error when returning an std::map of implicitly non-copyable structs on new versions of gcc

本文关键字:map std 错误 编译 结构 可复制 gcc 新版本 返回      更新时间:2023-10-16

我在新版本的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;
}

在这里,它也在具有实际错误的 ideone 上。基本上它归结为使用 ptrwrap 的已删除复制构造函数。哪。。。不应该发生。映射按值返回(即移动(,因此不能存在任何副本。

现在,在旧版本的gcc(我尝试了4.2和4.3(,我尝试过的所有版本的clang以及Visual Studio 2015上编译相同的代码没有问题。

奇怪的是,如果我删除持有者模板的显式副本并移动构造函数,它也会在 gcc 4.9+ 上编译。如果我将map更改为vectorunordered_map它也可以很好地编译(这是指向带有unordered_map的代码编译版本的链接(

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

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

要求:如果allocator_traits<allocator_type>::propagate_on_container_move_assignment::value falseTMoveInsertableXMoveAssignable。 [...]

(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;

长话短说:该错误基本上是由 stl_tree.h 中的这一行引起的。

_Rb_treestd::map的基础实现,其移动分配运算符定义中的该行基本上执行上述标准报价指定的检查。但是,它使用一个简单的if来完成,这意味着,即使满足条件,另一个分支也必须编译,即使它不会在运行时执行。缺少闪亮的新 C++17 if constexpr ,这应该使用类似标签调度的东西来实现(对于前两个条件 - 第三个是真正的运行时检查(,以避免实例化代码在 taken 分支之外。

然后,错误是由此行引起的,该行在value_type上使用std::move_if_noexcept。故事长话短说来了。

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

在初始代码中:

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

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

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

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

holder复制构造函数调用t复制构造函数:

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

如果tunique_ptr则不支持通过复制进行构造。

相关文章: