C++ - 如何在自定义模板化数据容器中的迭代器上使用 advance() 启用 ADL?

C++ - How to enable ADL with advance() on iterators in custom templated data container?

这是一个容器:

namespace container_namespace
{

template <class element_type, class element_allocator_type = std::allocator<element_type> >
class container
{
    // stuff

    class iterator
    {
        // stuff
    };
};

}

我在上面的什么地方定义了 advance(InputIt &, Distance N) 以便允许在我的 main() 中通过 ADL(参数相关查找)使用 advance()

int main(int argc, char **argv)
{
    using namespace std;
    using namespace container_namespace;

    container<int> c;

    // Add elements to c here

    container<int>::iterator it = c.begin();
    advance(it, 20);
}

并且选择了自定义 advance() 函数而不是 std::advance? 我已经看到在迭代器 class 中定义自定义 advance() 函数的示例,以及在名称空间中定义的示例,其中仅在迭代器 class 中声明了友谊。哪项对于启用 ADL 是正确的? SO 上的其他示例在这一点上并不清楚。

非限定名称查找将同时考虑普通查找找到的任何内容(在您的情况下,函数模板 std::advance)和 ADL 找到的内容(在您的情况下,advance(iterator&, Distance N))。它们将是由过载决议以同等理由考虑。

您的目标是确保您的自定义预付款更匹配,最简单的方法是确保它是非模板函数:模板 lose to non-templates if they are otherwise equally as good。如果您的 iterator 是一个 class 模板(或者,如图所示,是 class 模板的成员),您可以将您的 advance 设为在class 模板定义。

我认为最安全的方法是定义 friendcontaineriterator。这样定义的函数被放入namespace container_namespace,所以ADL可以找到:

namespace container_namespace {
    template <class element_type, class element_allocator_type = std::allocator<element_type> >
    class container {
        //...
        template <typename Diff>
        friend void advance(iterator&, Diff) {
            //...
        }
    };
}

DEMO

另一种选择是直接在 namespace container_namespace 中定义它。通过这种方式,您可以为所有容器提供通用实现 and/or 实现标签分派来处理不同的迭代器类别,就像在 std::advance 实现中所做的那样:

namespace container_namespace {
    template <typename Iter, typename Diff>
    void advance(Iter&, Diff) {
        std::cout << "ADL-ed advance\n";
    }
}

这种方法的问题在于,当 std::advance 在范围内时,它可能会导致歧义(感谢@TC): DEMO

另请注意,您不能按如下方式定义 advance

namespace container_namespace {
    template <typename element_type, typename element_allocator_type, typename Diff>
    void advance(typename container<element_type, element_allocator_type>::iterator&, Diff) {
        std::cout << "ADL-ed advance\n";
    }
}

因为它的第一个参数的类型会失败(参见 Non-deduced contexts)。

尽管发布的两个答案都是正确的(而且我都投了赞成票)我想我会更深入地介绍这个问题,以供将来发现此问题的任何人使用。

'Friend' 含义

对于初学者来说,'friend' 对 class 中的函数有不同的含义。如果它只是一个函数声明,那么它会将给定函数声明为 class 的友元,并允许访问它的 private/protected 成员。但是,如果它是一个函数实现,则意味着该函数是 (a) class 的友元,(b) 不是 class 的成员,并且 (c) 无法从任何封闭的命名空间中访问. IE。它成为一个全局函数,只能通过 argument-dependent lookup (ADL) 访问。

以下面的测试代码为例:

#include <iostream>
#include <iterator>

namespace nsp
{

template <class element_type, class element_allocator_type = std::allocator<element_type> >
class test_container
{
private:
    element_type numbers[50];
    friend class iterator;

public:
    class iterator : public std::iterator<std::bidirectional_iterator_tag, element_type>
    {
    private: 
        element_type *i;

        template <class distance_type>
        friend void advance(iterator &it, distance_type n);

        friend typename std::iterator_traits<iterator>::difference_type distance(const iterator &first, const iterator &last)
        {
            return last.i - first.i;
        }


    public: 

        iterator(element_type &_i)
        {
            i = &(_i);
        }

        element_type & operator *()
        {
            return *i;
        }

        element_type & operator = (const iterator source)
        {
            i = source.i;
            return *this;
        }

        bool operator != (const iterator rh)
        {
            return i != rh.i;
        }

        iterator & operator ++()
        {
            ++i;
            return *this;
        }

        iterator & operator --()
        {
            --i;
            return *this;
        }
    };


    iterator begin()
    {
        return iterator(numbers[0]);
    }


    iterator end()
    {
        return iterator(numbers[50]);
    }


    template <class distance_type>
    friend void advance(iterator &it, distance_type n)
    {
        it.i += 2 * n;
    }

};


}


int main(int argc, char **argv)
{
    nsp::test_container<int> stuff;

    int counter = 0;

    for (nsp::test_container<int>::iterator it = stuff.begin(); it != stuff.end(); ++it)
    {
        *it = counter++;
    }

    nsp::test_container<int>::iterator it = stuff.begin(), it2 = stuff.begin();

    using namespace std;

    cout << *it << endl;

    ++it;

    cout << *it << endl;

    advance(it, 2);

    cout << *it << endl;

    std::advance(it, 2);

    cout << *it << endl;

    int distance_between = distance(it2, it);

    cout << distance_between << endl;

    cin.get();

    return 0;
}

如果从 main() 中调用 advance(),ADL 将运行并且将调用 class 迭代器的自定义推进。但是,如果尝试 nsp::advance()nsp::test_container<int>::advance()stuff.advance(),这些将导致编译错误 ("no matching function call")。

模板问题

虽然调用非模板函数重载确实优先于模板函数重载,但这与 ADL 的使用无关。无论函数是模板还是非模板,都会调用特定类型的正确重载。另外,advance()特别需要一个距离类型的模板参数(int、long int、long long int等),不能跳过这个,因为我们不知道编译器会推断出什么类型从,说“1000000”,我们不知道程序员可能会在 advance() 处抛出什么样的类型。幸运的是,我们不需要担心偏特化,因为 std::advance() 与我们的自定义 advance 位于不同的命名空间中,并且可以简单地使用我们的硬编码迭代器类型实现我们自己的 advance(),如上例所示.

如果我们的迭代器本身是一个模板并接受参数,这仍然有效——我们只需将参数包含在高级模板中,并以这种方式对模板的迭代器类型进行硬编码。例如:

template <class element_type, class distance_type>
friend void advance(iterator<element_type>, distance_type distance);

更多 模板问题(旁注)

虽然这与 advance() 的实现没有具体关系,但它通常与 class 友元函数的实现有关。您会注意到在上面的示例中,我直接在迭代器 class 中实现了非模板函数 distance(),而 advance() 模板函数在迭代器 [=] 外部被声明为友元83=] 但在 test_container class 内。这是为了说明一个问题。

如果 class 是一个模板(或模板的一部分),你不能在它的友元 class 之外实现一个非模板友元函数,因为你的编译器会抛出一个错误。然而,模板的函数 advance() 可以 在 class 之外声明,仅在 friend class 中包含定义。 advance()功能也可以直接在朋友class中实现,我只是为了说明这一点而选择不这样做。

模板友元函数参数阴影

这与上面的示例无关,但对于进入模板友元函数的程序员来说可能是一个陷阱。如果你有一个模板 class 和一个在 class 上运行的友元函数,显然你将需要在函数定义中指定模板参数以及 class。例如:

template <class element_type, class element_allocator_type = std::allocator<element_type> >
class test_container
{
private:
    element_type numbers[50];

public:
    template <class element_type, class element_allocator_type>
    friend void do_stuff(test_container<element_type, element_allocator_type> &container)
    {
        numbers[1] = 5; // or whatever
    }

};

但是上述方法不起作用,因为编译器认为您对 'element_type' 和 'element_allocator_type' 使用相同的名称是对 [=80= 定义中首次使用的模板参数的重新定义],会报错。因此,您必须为它们使用不同的名称。 IE。这有效:

template <class element_type, class element_allocator_type = std::allocator<element_type> >
class test_container
{
private:
    element_type numbers[50];

public:
    template <class c_element_type, class c_element_allocator_type>
    friend void do_stuff(test_container<c_element_type, c_element_allocator_type> &container)
    {
        numbers[1] = 5; // or whatever
    }

};

就这些- 我希望任何遇到这个问题的人都能从中得到一些用处——大部分信息以某种方式、形状或形式分布在 Whosebug 中,但将它们整合在一起对新手来说很重要。

[更新:] 即使有上述所有内容,尽管它是 'correct',但仍可能不足以将 ADL 正确解析为正确的函数。这是因为 clang、microsoft visual studio 2010-2013,可能还有其他人,在解析复杂模板中的 ADL 时遇到困难,并且无论如何都可能崩溃或抛出错误。在这种情况下,明智的做法是简单地求助于迭代器 classes.

友好的标准容器函数

要利用 ADL,您需要两件事:

  • 让函数或函数模板位于正确的命名空间中
  • 函数或函数模板足够好

第一件事很简单,但第二件事需要一点小心。以下是您绝对应该 而不是 做的事情:

template<typename Element, typename Allocator>
struct vector {
    struct iterator {};
};

// trouble ahead!
template<typename Element, typename Allocator>
void advance(typename vector<Element, Allocator>::iterator it, int n)
{
    …
}

在这种特殊形式中,模板参数 ElementAllocatoradvancenon-deducible。换句话说,advance 仅在调用者传入这些参数时才可调用,例如ns::advance<int, std::allocator<int>>(it, n)。由于对 advance 的调用通常看起来不像这是一个非常糟糕的候选者,我们可以完全排除它。

内联好友

一个简短而有趣的解决方案是在 iterator 内定义一个内联友元函数。这项技术的关键在于,它没有定义函数模板,而是定义了一个函数——很明显 vector<E, A>::iterator 不是 class 模板,而是它本身是一个 class,每个 vector<E, A>专业化。

template<typename Element, typename Allocator>
struct vector {
    struct iterator {
         friend void advance(iterator it, int n)
         { … }
    };
};

Live On Coliru

advance 被 ADL 找到,因为它是正确的命名空间,而且由于它是一个非模板函数,它优于 std::advance。这片土地上一切都很好,不是吗?嗯,有一个限制,你不能取ns::advance的地址,事实上你根本不能给它命名。

平时可以把东西 by adding a namespace-scope declaration… except we can’t directly in our case because vector is a class template. In fact you run into many pitfalls when you first dwell into class templates and friends—e.g. you might see this reasonable FAQ item放上去想利用,却发现不适合

不是那么内联

如果你真的关心用户命名 advance 在非限定调用之外(例如获取地址或你有什么),我的建议是 'disentangle' iterator 来自 vector:

// might now need additional parameters for vector to fill in
template<typename Element>
struct vector_iterator;

template<typename Element, typename Allocator>
struct vector {
    using iterator = vector_iterator<Element>;
    …
};

特别是,如果我们遵循上一个常见问题解答项的建议,我们可能会得到以下形式的内容:

template<typename Element>
void advance(vector_iterator<Element> it, int n);

值得指出的是,这显然是一个函数模板,并且比例如std::advance 由于 partial ordering rules。部分排序几乎总是我的首选方法。