【问题标题】:std::pair: too restrictive constructor?std::pair: 过于严格的构造函数?
【发布时间】:2014-05-18 15:41:41
【问题描述】:

我偶然发现了 C++11 引入的新 std::pair 构造函数的一个令人惊讶的行为。我在使用std::pair<int, std::atomic<int>> 时观察到了这个问题,并且它发生了,因为std::atomic 既不可复制也不可移动。在以下代码中,为了简化,我将std::atomic<int> 替换为foobar。

以下代码编译良好,使用 GCC-4.9 和 Clang-3.5(有和没有 libc++):

struct foobar
{
    foobar(int) { } // implicit conversion
    // foobar(const foobar&) = delete;
};

std::pair<int, foobar> p{1, 2};

这种行为是预期的。但是,当我删除foobar 的复制构造函数时,编译失败。它适用于分段构造,但我认为这不是必需的,因为从int 到foobar 的隐式转换。我指的是具有以下签名的构造函数:

template <typename U, typename V>
pair(U&& u, V&& v);

你能解释一下,为什么 pair 构造函数如此严格,并且不允许对不可复制/不可移动类型进行隐式转换吗?

【问题讨论】:

  • @KerrekSB:我不明白你的意见。在我的示例中,两个成员变量都直接从int&amp;&amp; 初始化。不涉及 foobar 或 pair 的复制构造函数。
  • 嗯,我可能搞错了。我正在调查。
  • 所以,我尝试用我自制的配对类复制这个,但没有成功。我查看了错误,它指向 GCC 实现并提到了DR 811;奇怪的是,重载是template &lt;typename U1, typename = [some enable_if]&gt; pair(U1 &amp;&amp;, const T2 &amp;),它最终会生成一个副本(因为它构造了一个临时绑定到第二个参数)。但它不应该!
  • 我认为这是一个 dup :) 让我看看...问题是标准需要 is_convertible,这令人惊讶地需要可移动性。 stackoverflow.com/q/21405674/420683
  • @dyp:无论哪种方式,我认为这都不是问题:OP 的代码应该 会失败,而且确实会失败。 GCC 通过直接初始化而不是副本传递第一个参数这一事实并没有减少这一点。

标签: c++ c++11


【解决方案1】:

这是标准中的一个缺陷(我一开始没有发现它,因为它是为tuple制定的)。

https://wg21.link/lwg2051

进一步的讨论和提议的决议(2015 年 5 月在 Lenexa 投票支持 C++1z):

https://wg21.link/n4387


根本问题是pair 和tuple 的转换构造函数检查is_convertible,这需要一个可访问的复制/移动构造函数。

详细说明:std::pair&lt;T1, T2&gt; 和 std::tuple 的转换构造函数模板如下所示:

template<class U, class V>
constexpr pair(U&&, V&&);

但这太贪心了:当你尝试将它与不兼容的类型一起使用时,它会产生一个硬错误,并且std::is_constructible&lt;pair&lt;T1, T2&gt;, U, V&gt;::value 将永远是true,因为这个构造函数模板的声明可以为 any 类型 U 和 V。因此,我们需要限制这个构造函数模板:

template<class U, class V,
    enable_if_t<check_that_we_can_construct_from<U, V>::value>
>
constexpr pair(U&& u, V&& v)
    : t1( forward<U>(u) ), t2( forward<V>(v) )
{}

注意tx( forward&lt;A&gt;(a) ) 可以调用explicit 构造函数。因为pair 的构造函数模板未标记为显式,我们必须限制它不在初始化其数据成员时执行内部显式转换 .因此,我们使用is_convertible:

template<class U, class V,
    std::enable_if_t<std::is_convertible<U&&, T1>::value &&
                     std::is_convertible<V&&, T2>::value>
>
constexpr pair(U&& u, V&& v)
    : t1( forward<U>(u) ), t2( forward<V>(v) )
{}

在 OP 的情况下,没有隐式转换:类型是不可复制的,这使得定义 隐式可转换性的测试格式错误:

// v is any expression of type `int`
foobar f = v; // definition of implicit convertibility

这个根据标准的复制初始化表格在右侧产生一个临时的,用v初始化:

foobar f = foobar(v);

右侧应理解为隐式转换(因此不能调用explicit 构造函数)。但是,这需要将右侧的临时文件复制或移动到f(C++1z 之前,请参阅p0135r0)。

总结一下:int 不能隐式转换为 foobar,因为隐式转换的定义方式需要可移动性,因为 RVO 不是强制性的。 pair&lt;int, foobar&gt; 不能从 {1, 2} 构造,因为这个 pair 构造函数模板不是 explicit,因此需要隐式转换。


explicit 与 Improvements on pair and tuple 中提出的隐式转换问题的更好解决方案是使用 explicit 魔术:

构造函数是explicit当且仅当is_convertible<U&&, first_type>::value是false或is_convertible&lt;V&amp;&amp;, second_type&gt;::value 是false。

通过此更改,我们可以将隐式可转换性 (is_convertible) 的限制放松为“显式可转换性” (is_constructible)。实际上,在这种情况下,我们得到以下构造函数模板:

template<class U, class V,
    std::enable_if_t<std::is_constructible<U&&, int>::value &&
                     std::is_constructible<V&&, foobar>::value>
>
explicit constexpr pair(U&&, V&&);

这足以使std::pair&lt;int, foobar&gt; p{1, 2}; 有效。

【讨论】:

  • 有趣。第二个链接引用的文档正是讨论了我观察到的问题。但我不明白第一个链接。我看到它与第二个链接有关,但它是否也与我的问题有关?
  • @nosid 之类的。 is_convertible 也是显式构造函数的 false,因此它具有相同的潜在问题。
  • 你能否详细说明那里描述的问题,以及它是如何解决的(据说最近解决了,所以我猜这在 C++17 中应该可以工作。)
  • @einpoklum 我已经“稍微”阐述了;)
  • N4528 错过了此更改。它位于this commit。是预期的吗?
【解决方案2】:

测试您的代码,删除复制构造函数,我得到

[h:\dev\test\0082] > g++ foo.cpp 在 h:\bin\mingw\include\c++\4.8.2\utility:70:0 包含的文件中, 从 foo.cpp:1: h:\bin\mingw\include\c++\4.8.2\bits\stl_pair.h: 在 'constexpr std::pair::pair(_U1&&, const _T2&) [with _U1 = int; = 无效; _T1 = 整数; _T2 = foobar]': foo.cpp:12:34:从这里需要 h:\bin\mingw\include\c++\4.8.2\bits\stl_pair.h:134:45: 错误:使用已删除的函数 'foobar::foobar(const foobar&)' : 第一个(std::forward<_u1>(__x)), 第二个(__y) { } ^ foo.cpp:6:5: 错误:在这里声明 foob​​ar(const foobar&) = 删除; ^ [h:\dev\test\0082] > cl foo.cpp foo.cpp [h:\dev\test\0082] > _

上面提到的构造函数

pair(_U1&&, const _T2&)

标准没有规定。


附录:如下所示,代码仅使用为 pair 类定义的标准构造函数就可以正常工作:

#include <utility>

struct foobar
{
    foobar(int) { } // implicit conversion
    foobar(const foobar&) = delete;
};

namespace bah {
    using std::forward;
    using std::move;

    struct Piecewise_construct_t {};

    template <class T1, class T2>
    struct Pair {
        typedef T1 first_type;
        typedef T2 second_type;
        T1 first;
        T2 second;

        //Pair(const Pair&) = default;
        //Pair(Pair&&) = default;

        /*constexpr*/ Pair(): first(), second() {}

        Pair(const T1& x, const T2& y)
            : first( x ), second( y )
        {}

        template<class U, class V> Pair(U&& x, V&& y)
            : first( forward<U>( x ) ), second( forward<V>( y ) )
        {}

        template<class U, class V> Pair(const Pair<U, V>& p)
            : first( p.first ), second( p.second )
        {}

        template<class U, class V> Pair(Pair<U, V>&& p)
            : first( move( p.first ) ), second( move( p.second ) )
        {}

        //template <class... Args1, class... Args2>
        //Pair(Piecewise_construct_t,
        //tuple<Args1...> first_args, tuple<Args2...> second_args);
        //
        //Pair& operator=(const Pair& p);
        //template<class U, class V> Pair& operator=(const Pair<U, V>& p);
        //Pair& operator=(Pair&& p) noexcept(see below);
        //template<class U, class V> Pair& operator=(Pair<U, V>&& p);
        //void swap(Pair& p) noexcept(see below);
    };
}

auto main()
    -> int
{
    bah::Pair<int, foobar> p{1, 2};
};
[h:\dev\test\0082] > g++ bar.cpp [h:\dev\test\0082] > _

重要勘误表。
正如@dyb 在 cmets 中指出的那样,虽然标准的“requires”子句指的是std::is_constructible(该对的项目必须可以从参数构造),但遵循Defect Report 811 的决议的“remarks”子句指的是可转换性:

C++11 §20.3.2/8:
“备注:如果U 不能隐式转换为first_type 或V 不能隐式转换为second_type,则此构造函数不应参与重载决议。”

因此,虽然现在这可以说是标准中的一个缺陷,但从正式的角度来看,代码不应该编译。

【讨论】:

  • 请注意,OP 使用 G++ V4.9,而不是 V4.8.2。 4.9 比 4.8 更接近 C++11,也许这就是你在他的代码上测试失败的原因。
  • @user465139:从问题cmets来看,4.9的问题是一样的。它引入了标准中没有的构造函数。
猜你喜欢
  • 1970-01-01
  • 2012-03-05
  • 2023-03-18
  • 2012-02-12
  • 1970-01-01
  • 2017-12-23
  • 2021-03-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多