比较两个map::迭代器:为什么需要使用std::pair的复制构造函数?

19
以下非常简单的代码在C++98模式下可以编译和链接而不会有警告,但在C++11模式下会产生一个难以理解的编译错误。
#include <map>

struct A {
    A(A& ); // <-- const missing
};

int main() {
    std::map<int, A> m;
    return m.begin() == m.end(); // line 9
}

"

-std=c++11的错误是,gcc版本为4.9.0 20140302 (experimental) (GCC):

ali@X230:~/tmp$ ~/gcc/install/bin/g++ -std=c++11 cctor.cpp 
在文件 /home/ali/gcc/install/include/c++/4.9.0/bits/stl_algobase.h:64:0 中包含,
                 来自 /home/ali/gcc/install/include/c++/4.9.0/bits/stl_tree.h:61,
                 来自 /home/ali/gcc/install/include/c++/4.9.0/map:60,
                 来自 cctor.cpp:1:
/home/ali/gcc/install/include/c++/4.9.0/bits/stl_pair.h: 在‘struct std::pair’的实例化中:
cctor.cpp:9:31:   从这里开始
/home/ali/gcc/install/include/c++/4.9.0/bits/stl_pair.h:127:17: 错误:‘constexpr std::pair::pair(const std::pair&) [with _T1 = const int; _T2 = A]’被声明为const引用,但隐式声明将不使用const
       constexpr pair(const pair&) = default;
                 ^

clang版本为3.5 (trunk 202594)

"
ali@X230:~/tmp$ clang++ -Weverything -std=c++11 cctor.cpp 
文件包含自:cctor.cpp:1:
文件包含自:/usr/lib/gcc/x86_64-linux-gnu/4.7/../../../../include/c++/4.7/map:60:
文件包含自:/usr/lib/gcc/x86_64-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_tree.h:63:
文件包含自:/usr/lib/gcc/x86_64-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_algobase.h:65:
/usr/lib/gcc/x86_64-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_pair.h:119:17: 错误:显式默认的复制构造函数的参数是const,但一个成员或基类要求它为非const
      constexpr pair(const pair&) = default;
                ^
在此请求模板类'std::pair'的实例化
    return m.begin() == m.end(); // line 9
                     ^
1 error generated.
我一直在查看位于bits/stl_tree.h的代码,但我不明白为什么它要尝试实例化std::pair
为什么它需要C++11中的std::pair复制构造函数?
注意:上述代码摘自Equality operator (==) unsupported on map iterators for non-copyable maps

解决方案

这里有两个不幸的问题。

错误信息质量差:第8行应该已经给出了编译错误,尽管错误信息只在抱怨第9行。如果第8行出现错误将会非常有帮助,理解真正的问题会更容易。如果这个问题在gcc / clang trunk中仍然存在,我可能会提交一个错误报告/功能请求。

另一个问题是ecatmur所写的。考虑以下代码:

struct A {
    A() = default;
    A(A& ); // <-- const missing
};

template<class T>
struct B {
    B() = default;
    B(const B& ) = default;
    T t;
};

int main() {
  B<A> b;  
}

编译失败。尽管复制构造函数在任何地方都不需要,但它仍然被实例化,因为它是在类的内部默认内联的;这会导致编译错误。可以通过将复制构造函数移出类的主体来修复此问题:

template<class T>
struct B {
    B() = default;
    B(const B& );
    T t;
};

template <class T>
B<T>::B(const B& ) = default;

一切都没问题。不幸的是,std::pair有一个默认的内联复制构造函数。

1
在coliru上,当map仅被定义时就已经失败了,不需要包括比较或begin/end - dyp
在这个例子中,如果仅声明了map,则会失败。链接 - 4pie0
@dyp 正确。那样可能会更明显一些。奇怪的是错误消息只在迭代器比较时出现。 - Ali
1
std::pair<int, A> p;就足够了。 - Yakk - Adam Nevraumont
1
clang/libc++没有问题:http://coliru.stacked-crooked.com/a/d9546aed80a496c3(请注意,您的输出表明您正在使用带有gcc 4.7库的clang) - Cubbi
@Cubbi 啊,谢谢,我一直在想clang会如何处理libc++。请在这里阅读。很高兴知道我可以在Coliru上使用libc++测试clang,我之前不知道。 - Ali
3个回答

4
std::pair的复制构造函数在这种情况下并不是必需的,但是由于它在std::pair的声明中被默认定义为内联函数,所以它会随着std::pair本身的实例化而自动实例化。
标准库可以提供一个非内联的默认复制构造函数定义。
template<class _T1, class _T2>
  struct pair
  {
// ...
    constexpr pair(const pair&);
// ...
  };
// ...
template<class _T1, class _T2>
constexpr pair<_T1, _T2>::pair(const pair&) = default;

但是这不符合严格的标准要求(20.3.2条款),其中拷贝构造函数的默认定义是内联的:

  constexpr pair(const pair&) = default;

2
抱歉,为什么它会自动实例化?对于其他“模板”方法,即使函数写在类内部,只有在调用时才会实例化。现在有措辞说“=default”会在不被调用的情况下实例化吗? - Yakk - Adam Nevraumont
@Ali Coliru正在使用一个旧版本的libc++,它没有使用= default实现pair复制构造函数;如果我使用更新的libc++版本,那么clang会出现与gcc/libstdc++相同的问题。 - ecatmur
@ecatmur 我知道了,感谢提供这些信息。"看起来clang和g++特殊处理了复制构造函数 - 我甚至找不到任何措辞表明默认无法隐式定义的复制构造函数是错误的(8.4.2p4只是说最终它们被定义为已删除)。这可能是个bug(在两个编译器中都有)吗?" 你有没有找出它是否是一个bug? - Ali
@Yakk 我认为8.4.2p2是导致这个问题的原因:“如果一个函数在其第一次声明上被显式地默认,则[...]一个复制构造函数[...]应该具有与隐式声明相同的参数类型”。因此,编译器需要检查参数类型是否“正确”,即使最终并没有调用复制构造函数。 - ecatmur
@Ali 看起来编译器遵循了标准,不幸的是。 - ecatmur
显示剩余7条评论

2

在尝试减少错误之后,我认为我找到了问题所在。首先,似乎不需要比较来使程序失效。然后,错误消息包含了dtor,所以我尝试不实例化dtor。结果如下:

#include <map>

struct A {
    A(A& ); // <-- const missing
};

int main() {
    std::map<int, A>* m = new std::map<int, A>();
    // note: dtor not (necessarily?) instantiated
}

但输出信息仍然包含,现在是调用m的构造函数的行:

error: the parameter for this explicitly-defaulted copy constructor is const, but a member or base requires it to be non-const

 constexpr pair(const pair&) = default;

涉及到[dcl.fct.def.default]/4的提示

用户提供的显式默认函数(即在其首次声明后显式默认的函数)在其显式默认的点上被定义;如果这样的函数被隐式定义为已删除,则程序是非法的。

[强调我的]

如果我所认为的[class.copy]/11说这个构造函数应该被定义为已删除,那么它会立即被定义为已删除-而不仅仅是在它被odr-used时。因此,不需要实例化就可以使程序非法。


我认为[class.copy]/11应该意味着这个函数已被删除;然而,尽管它提到了重载决议,但它并没有明确说明函数必须是可行的。 - dyp
好的,谢谢。如果您查看我发布的错误信息,您会发现这些信息也包含了您收到的相同错误消息。还有一个问题:在问题中发布的代码中,编译器仅对迭代器比较发出警告符合标准吗? - Ali
@Ali 嗯,我认为一旦程序格式错误,只需要一个诊断信息(而且我不认为对其内容有任何要求)。 - dyp
我本来期望在第一行编译失败时会收到一个错误消息。我知道只需要一个错误消息,但是只抱怨第二个有问题的行是否允许?这会导致混淆,我认为这是一个非常糟糕的错误消息。 - Ali
@Ali 我同意诊断应该得到改进。但正如我所说,标准对诊断消息的内容没有任何要求,[intro.compliance]/2(行号<=内容)。 - dyp
1
好的,那我可能会提交一个错误报告/功能请求,因为我觉得这很令人困惑。 - Ali

2

std::map使用std::pair来存储键值对,其中键(第一个元素)是const

编译器错误与std::pair所需的复制构造函数有关,即使它没有被使用(我认为它没有被使用)。

std::pair<int, A>必须被生成。这是在调用map::begin时首先需要的。 由于该类型没有给出显式的复制构造函数,因此使用隐式构造函数。

如果T的所有非静态成员(类型S)都具有复制构造函数S::S(const S&)(同样的要求也适用于T的基类型的复制构造函数),则隐式构造函数将具有签名T::T(const T&) 只有当。 否则,将使用带有签名T::T(T&)的复制构造函数。

A的复制构造函数不满足此要求,因此std::pair::pair对于STL具有错误的签名,STL需要T::T(const T&)。


我不明白它如何回答这个问题。 - Ali
同意。为什么需要制作副本? - Eric Finn
我本来就走错了方向,现在编辑了我的答案,以表达我认为正在发生的事情。 - xen-0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接