首页 文章

为什么在C 11中使用非成员开始和结束函数?

提问于
浏览
174

每个标准容器都有一个 beginend 方法,用于返回该容器的迭代器 . 但是,C 11显然引入了名为std::beginstd::end的自由函数,它们调用 beginend 成员函数 . 所以,而不是写作

auto i = v.begin();
auto e = v.end();

你会写的

using std::begin;
using std::end;
auto i = begin(v);
auto e = end(v);

在他的演讲中,Herb Sutter说当你想要一个容器的开始或结束迭代器时,你应该总是使用自由函数 . 但是,他没有详细说明你为什么要这样做 . 查看代码,它可以为您节省一个字符 . 因此,就标准容器而言,自由函数似乎完全没用 . Herb Sutter表示非标准容器有好处,但他再次没有详细说明 .

那么,问题是 std::beginstd::end 的自由函数版本究竟做了什么,而不是调用它们相应的成员函数版本,为什么要使用它们呢?

6 回答

  • 4

    你如何在C阵列上调用 .begin().end()

    自由函数允许更通用的编程,因为它们可以在之后添加到您无法更改的数据结构上 .

  • 34

    使用 beginend free函数可以添加一层间接 . 通常这样做是为了提供更大的灵活性 .

    在这种情况下,我可以想到一些用途 .

    最明显的用途是C数组(不是c指针) .

    另一种方法是尝试在不符合要求的容器上使用标准算法(即容器缺少 .begin() 方法) . 假设你不能只修复容器,下一个最好的选择是重载 begin 函数 . Herb建议您始终使用 begin 函数来提高代码的一致性和一致性 . 而不必记住哪些容器支持方法 begin 以及哪些需要函数 begin .

    顺便说一下,下一个C rev应该复制D's pseudo-member notation . 如果未定义 a.foo(b,c,d) ,则会尝试 foo(a,b,c,d) . 它只是一点点语法糖来帮助我们那些喜欢主语然后动词排序的穷人 .

  • 15

    要回答你的问题,默认情况下,自由函数begin()和end()除了调用容器的成员.begin()和.end()函数之外别无其他 . 从 <iterator> 开始,当您使用任何标准容器(如 <vector><list> 等)时自动包含,您将得到:

    template< class C > 
    auto begin( C& c ) -> decltype(c.begin());
    template< class C > 
    auto begin( const C& c ) -> decltype(c.begin());
    

    你问的第二部分是为什么更喜欢自由函数,如果他们所做的只是调用成员函数 . 这实际上取决于示例代码中的对象 v . 如果v的类型是标准容器类型,如 vector<T> v; 那么无论你使用free或member函数都没关系,它们会做同样的事情 . 如果您的对象 v 更通用,如下面的代码所示:

    template <class T>
    void foo(T& v) {
      auto i = v.begin();     
      auto e = v.end(); 
      for(; i != e; i++) { /* .. do something with i .. */ } 
    }
    

    然后使用成员函数打破T = C数组,C字符串,枚举等的代码 . 通过使用非成员函数,您可以宣传人们可以轻松扩展的更通用的界面 . 通过使用免费功能界面:

    template <class T>
    void foo(T& v) {
      auto i = begin(v);     
      auto e = end(v); 
      for(; i != e; i++) { /* .. do something with i .. */ } 
    }
    

    该代码现在适用于T = C数组和C字符串 . 现在编写少量的适配器代码:

    enum class color { RED, GREEN, BLUE };
    static color colors[]  = { color::RED, color::GREEN, color::BLUE };
    color* begin(const color& c) { return begin(colors); }
    color* end(const color& c)   { return end(colors); }
    

    我们也可以使您的代码与可迭代枚举兼容 . 我认为Herb的主要观点是使用自由函数就像使用成员函数一样简单,它使代码向后兼容C序列类型,并向前兼容非stl序列类型(以及future-stl类型!),对其他开发人员来说成本低 .

  • 31

    虽然非成员函数不为标准容器提供任何好处,但使用它们会强制执行更一致和灵活的样式 . 如果您某个时候想要扩展现有的非std容器类,那么您宁愿定义自由函数的重载,而不是更改现有类的定义 . 因此,对于非std容器,它们非常有用,并且总是使用自由函数使您的代码更加灵活,因为您可以更容易地用非std容器替换std容器,并且底层容器类型对代码更透明,因为它支持更广泛的容器实现 .

    但当然,这总是必须适当加权,而且抽象也不好 . 尽管使用自由函数并不是过度抽象,但它仍然破坏了与C 03代码的兼容性,C 03代码在这个年轻的C 11代码可能仍然是一个问题 .

  • 4

    考虑具有包含类的库的情况:

    class SpecialArray;
    

    它有2种方法:

    int SpecialArray::arraySize();
    int SpecialArray::valueAt(int);
    

    迭代它需要从这个类继承的值,并为以下情况定义 begin()end() 方法

    auto i = v.begin();
    auto e = v.end();
    

    但如果你总是使用

    auto i = begin(v);
    auto e = end(v);
    

    你可以这样做:

    template <>
    SpecialArrayIterator begin(SpecialArray & arr)
    {
      return SpecialArrayIterator(&arr, 0);
    }
    
    template <>
    SpecialArrayIterator end(SpecialArray & arr)
    {
      return SpecialArrayIterator(&arr, arr.arraySize());
    }
    

    其中 SpecialArrayIterator 是这样的:

    class SpecialArrayIterator
    {
       SpecialArrayIterator(SpecialArray * p, int i)
        :index(i), parray(p)
       {
       }
       SpecialArrayIterator operator ++();
       SpecialArrayIterator operator --();
       SpecialArrayIterator operator ++(int);
       SpecialArrayIterator operator --(int);
       int operator *()
       {
         return parray->valueAt(index);
       }
       bool operator ==(SpecialArray &);
       // etc
    private:
       SpecialArray *parray;
       int index;
       // etc
    };
    

    现在 ie 可以合法地用于迭代和访问SpecialArray的值

  • 149

    std::beginstd::end 的一个好处是它们可以作为实现外部类标准接口的扩展点 .

    如果你想使用带有基于范围的for循环或模板函数的 CustomContainer 类,它需要 .begin().end() 方法,你显然必须实现这些方法 .

    如果该类确实提供了这些方法,那不是问题 . 如果没有,你必须修改它* .

    这并不总是可行的,例如在使用外部库时,尤其是商业和闭源库 .

    在这种情况下, std::beginstd::end 会派上用场,因为可以在不修改类本身的情况下提供迭代器API,而是重载自由函数 .

    Example: 假设你想实现 count_if 函数,它接受一个容器而不是一对迭代器 . 这样的代码可能如下所示:

    template<typename ContainerType, typename PredicateType>
    std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
    {
        using std::begin;
        using std::end;
    
        return std::count_if(begin(container), end(container),
                             std::forward<PredicateType&&>(predicate));
    }
    

    现在,对于您要使用此自定义 count_if 的任何类,您只需添加两个自由函数,而不是修改这些类 .

    现在,C有一个名为Argument Dependent Lookup(ADL)的机制这种方法更加灵活 .

    简而言之,ADL意味着,当编译器解析非限定函数(即没有命名空间的函数,如 begin 而不是 std::begin )时,它还将考虑在其参数的名称空间中声明的函数 . 例如:

    namesapce some_lib
    {
        // let's assume that CustomContainer stores elements sequentially,
        // and has data() and size() methods, but not begin() and end() methods:
    
        class CustomContainer
        {
            ...
        };
    }
    
    namespace some_lib
    {    
        const Element* begin(const CustomContainer& c)
        {
            return c.data();
        }
    
        const Element* end(const CustomContainer& c)
        {
            return c.data() + c.size();
        }
    }
    
    // somewhere else:
    CustomContainer c;
    std::size_t n = count_if(c, somePredicate);
    

    在这种情况下,限定名称是 some_lib::beginsome_lib::end 并不重要 - 因为 CustomContainer 也在 some_lib:: 中,编译器将使用 count_if 中的那些重载 .

    这也是在 count_if 中拥有 using std::begin;using std::end; 的原因 . 这允许我们使用不合格的 beginend ,因此允许ADL and 允许编译器在没有找到其他替代选项时选择 std::beginstd::end .

    我们可以吃 Cookies 并吃 Cookies - 我 . 即有一种方法可以提供 begin / end 的自定义实现,而编译器可以回退到标准的实现 .

    一些说明:

    • 出于同样的原因,还有其他类似的功能: std::rbegin / rendstd::sizestd::data .

    • 正如其他答案所提到的, std:: 版本对裸阵列有重载 . 那个's useful, but is simply a special case of what I' ve描述如上 .

    • 在编写模板代码时使用 std::begin 和朋友特别好,因为这会使这些模板更通用 . 对于非模板,您也可以使用方法(如果适用) .

    P. S.我知道这篇文章已有近7年的历史了 . 我遇到它是因为我想回答一个被标记为重复的问题并发现这里没有答案提及ADL .

相关问题