【问题标题】:How to differentiate fill constructor and range constructor in C++11?如何区分 C++11 中的填充构造函数和范围构造函数?
【发布时间】:2021-09-13 03:09:19
【问题描述】:

我怀疑这个webpage中给出的std::vector(以及许多其他STL类型)的填充构造函数和范围构造函数的原型是不正确的,所以我实现了一个NaiveVector来模仿这两个原型。

我的代码是:

#include <iostream>
#include <vector>
using namespace std;

template <typename T>
struct NaiveVector {
  vector<T> v;
  NaiveVector(size_t num, const T &val) : v(num, val) { // fill
    cout << "(int num, const T &val)" << endl;
  }

  template <typename InputIterator>
  NaiveVector(InputIterator first, InputIterator last) : v(first, last) { // range
    cout << "(InputIterator first, InputIterator last)" << endl;
  }

  size_t size() const { return v.size(); }
};

int main() {
  NaiveVector<int> myVec1(5,1);                   // A
  cout << "size = " << myVec1.size() << endl;
  for (auto n : myVec1.v) { cout << n << " "; }
  cout << endl;

  cout << "-----" << endl;

  vector<int> vec({1,2,3,4,5});               
  NaiveVector<int> myVec2(vec.begin(), vec.end());// B
  cout << "size = " << myVec2.size() << endl;
  for (auto n : myVec2.v) { cout << n << " "; }
  cout << endl;
}

输出是:

$ g++ overload.cc -o overload -std=c++11
$ ./overload
(InputIterator first, InputIterator last) // should be: (int num, const T &val)
size = 5
1 1 1 1 1
-----
(InputIterator first, InputIterator last)
size = 5
1 2 3 4 5

正如我从一开始就怀疑的那样,编译器无法正确区分这两个构造函数。那么我的问题是:std::vector的填充构造函数和范围构造函数如何区分?

改写:如何实现这个NaiveVector的两个构造函数?

这个问题似乎与this 问题重复,但答案并不令人满意。此外,C++11 本身不提供is_iterator&lt;&gt;..(MSVC 有很多 hack)。

编辑:允许将右值绑定到常量左值引用,因此NaiveVector 的第一个构造函数对A 有效。

【问题讨论】:

  • “我怀疑这个网页中给出的 std::vector (以及许多其他 STL 类型)的填充构造函数和范围构造函数的原型不正确,” - 好吧,这不会让我感到惊讶,但为什么不简单地使用标准库中的正确原型呢?
  • InputIterator 仅表示模板参数名称。与std::vector&lt;T&gt;::iterator无关。
  • @user0042 你是对的,但我并不是说InputIterator 应该是std::vector&lt;T&gt;::iterator。它确实可以是任何满足input iterator 要求的类。
  • cplusplus.com 是垃圾。 cppreference.com 的更好参考。特别注意:“此重载仅在 InputIt 满足 InputIterator 时才参与重载解决,以避免与重载 (2) 产生歧义。”这通常通过SFINAE 实现

标签: c++ c++11 constructor overloading


【解决方案1】:

C++03

[lib.sequence.reqmts]/9

对于本节和第 21 节中定义的每个序列:

  • 构造函数

    template <class InputIterator>
    X(InputIterator f, InputIterator l, const Allocator& a = Allocator())
    

    应具有与以下相同的效果:

    X(static_cast<typename X::size_type>(f),
      static_cast<typename X::value_type>(l),
      a)
    

    如果InputIterator 是一个整数类型。

...

[lib.sequence.reqmts]/11

序列实现者可以满足此要求的一种方法是将成员模板专门用于 每个整数类型。还存在不太繁琐的实现技术。

换句话说,标准规定,如果范围构造函数被重载决议选择,但“迭代器”类型实际上是一个整数类型,它必须通过转换其参数类型来委托填充构造函数以强制后者完全匹配。

C++11/C++14

[sequence.reqmts]/14

对于本节和第 21 节中定义的每个序列容器:

  • 如果构造函数

    template <class InputIterator>
    X(InputIterator first, InputIterator last,
      const allocator_type& alloc = allocator_type())
    

    使用不符合输入迭代器条件的类型InputIterator 调用,则构造函数不应参与重载决议。 ...

[sequence.reqmts]/15

未指定实现确定类型不能是输入迭代器的程度, 除了作为最低限度的整数类型不应有资格作为输入迭代器。

这或多或少是标准提示您使用 SFINAE 的方式(与 C++03 相比,它在 C++11 中可靠地工作)。

C++17

措辞类似,只是关于整数类型不是迭代器的段落已移至[container.requirements.general]/17

结论

您可以将范围构造函数编写成如下所示:

template <typename InputIterator,
          typename = std::enable_if<is_likely_iterator<InputIterator>>::type>
NaiveVector(InputIterator first, InputIterator last)

is_iterator 帮助器模板可能会简单地取消整数类型的资格并接受所有其他类型,或者它可能会做一些更复杂的事情,例如检查是否有一个 std::iterator_traits 特化表明该类型是输入迭代器(或更严格)。见libstdc++ sourcelibc++ source

这两种方法都与标准在 C++11 及更高版本中 std::vector 的强制行为一致。建议使用后一种方法(并且将与真正的 std::vector 在典型实现上可能执行的操作一致),因为如果您传递可隐式转换为填充构造函数预期类型的​​参数,您会希望该类会“做正确的事”而不是选择范围构造函数,只是让它无法编译。 (虽然我怀疑这并不常见。)相对于 C++03 std::vector,它是一个“符合标准的扩展”,因为在这种情况下,构造函数调用不会编译。

【讨论】:

  • 为了全面起见,答案here给出了一个简单的代码和可运行的在线实现。
【解决方案2】:

如果您仔细阅读http://en.cppreference.com/w/cpp/container/vector/vector 的文档,您会注意到以下内容(重点是我的):

4) 使用范围[first, last) 的内容构造容器。

如果InputIt 是整数类型,则此构造函数与vector(static_cast&lt;size_type&gt;(first), static_cast&lt;value_type&gt;(last), a) 的效果相同。 (直到 C++11)

此重载仅在 InputIt 满足 InputIterator 时参与重载解析,以避免与重载混淆 (2)

混淆不在于std::vector,而是您对调用哪个构造函数的期望。您的代码没有检查以确保仅在满足std::vector 的上述条件时才调用第二个构造函数。

在你的情况下,当你使用

NaiveVector<int> myVec1(5,1);

第二个构造函数可以通过使用int作为模板参数来调用,不需要任何转换。编译器需要将int 转换为size_t 才能调用第一个构造函数。因此,第二个构造函数更合适。

【讨论】:

    【解决方案3】:

    您可以尝试添加一些类型特征检查来验证第二个构造函数的参数是否是 T 元素上的迭代器:

    template
    <
        typename InputIterator
    ,   typename = typename ::std::enable_if
        <
            ::std::is_same
            <
                T &
            ,   typename ::std::remove_const
                <
                    decltype(*(::std::declval< InputIterator >()))
                >::type
            >::value
        ,  void
        >::type
    >
    NaiveVector(InputIterator first, InputIterator last) : v(first, last)
    {
        cout << "(InputIterator first, InputIterator last)" << endl;
    }
    

    Run this code online

    【讨论】:

    • std::vector&lt;T&gt; 的构造函数接受 value_typeT 不同但可转换为 T 的迭代器。
    • @IgorTandetnik 所以你的意思是这个答案中给出的代码应该进一步概括?
    • @user8385554 是的,可以使用::std::is_convertible 调整此代码以实现该行为。
    • 在您的解决方案中存在一个错误:如果 InputIt 是一个 const_iterator,则使用 * 取消引用它会产生一个 const T &amp;,其 const 不能被 std::remove_const 抛弃.因此将其与T &amp; 比较将返回false,从而导致编译器选择填充构造函数。除了将两个 is_same 条件与 OR 运算符组合在一起之外,似乎没有更好的解决方法,一个与 T &amp; 比较,另一个与 const T &amp; 比较。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-09
    • 2013-12-24
    • 1970-01-01
    • 2016-03-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多