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

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

本文关键字:迭代器 advance ADL 启用 自定义 数据 C++      更新时间:2023-10-16

这是一个容器:

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)的位置,以便允许通过 ADL(依赖于参数的查找(在我的main()中使用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?我已经看到了在迭代器类中定义的自定义advance()函数的示例,以及在命名空间中定义它的示例,其中仅在迭代器类中声明了友谊。启用 ADL 的正确方法是什么?关于SO的其他例子在这一点上并不清楚。

我认为

最安全的方法是将其定义为friend containeriterator。以这种方式定义的函数被放入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) {
            //...
        }
    };
}

演示

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

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

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

另请注意,不能按如下方式定义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 advancen";
    }
}

因为它的第一个参数的类型会失败(请参阅非推导上下文(。

非限定名称查找将考虑普通查找找到的任何内容(在您的情况下,函数模板std::advance(和 ADL 找到的内容(在您的情况下,advance(iterator&, Distance N) .它们将通过基于相同理由的过载解决方案来考虑。

您的目标是确保您的自定义高级版是更好的匹配,最简单的方法是确保它是一个非模板函数:如果模板在其他方面同样好,则它们会输给非模板。如果iterator是类模板(或者如图所示,是类模板的成员(,则可以将advance设置为类模板定义中定义的非模板友元。

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

"朋友"的含义

对于初学者来说,"friend"在类中的函数上有不同的含义。如果它只是一个函数声明,那么它将给定的函数声明为类的友元,并允许访问它的私有/受保护成员。但是,如果它是一个函数实现,则意味着该函数是(a(类的友元,(b(不是类的成员,(c(无法从任何封闭命名空间中访问。即。它成为一个全局函数,只能通过依赖于参数的查找 (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 将起作用,并且将调用类迭代器的自定义前进。但是,如果尝试nsp::advance()nsp::test_container<int>::advance()stuff.advance(),这些将导致编译错误("没有匹配的函数调用"(。

模板问题

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

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

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

更多模板问题(旁注(

虽然这与advance()的实现没有特别关系,但它与一般的类友元函数的实现有关。您会注意到,在上面的示例中,我直接在迭代器类中实现了非模板函数distance(),而advance() template'd 函数被声明为迭代器类之外但在test_container类中的友元。这是为了说明一点。

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

模板友元函数参数阴影

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

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"使用相同的名称是对test_container定义中首次使用的模板参数的重新定义,并且会引发错误。因此,您必须为这些使用不同的名称。即。这有效:

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
    }
};

就这样-我希望任何偶然发现它的人都能从中得到一些用处 - 大部分信息以某种方式、形状或形式分布在 stackoverflow 中,但将它们组合在一起对新手来说很重要。

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

您需要两件事来利用 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)
{
    …
}

在这种特定形式中,事实证明,模板参数ElementAllocator advance是不可推导的。换句话说,只有当调用方传入这些参数时,advance才是可调用的,例如 ns::advance<int, std::allocator<int>>(it, n) .由于对advance的呼吁通常看起来不像这是一个非常糟糕的候选人,我们可以完全排除它。

内联好友

一个

简短而甜蜜的解决方案是在 iterator 中内联定义一个友元函数。这种技术的关键在于,它不定义函数模板,而定义函数 - 非常多vector<E, A>::iterator不是类模板,而是它本身是一个类,每个vector<E, A>专用化一个。

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

住在科里鲁

ADL 可以找到advance,因为它是正确的命名空间,并且由于它是一个非模板函数,因此它优先于 std::advance .这片土地上一切都很好,不是吗?好吧,有一个限制,你不能拿ns::advance的地址,实际上你根本无法命名它。

您通常可以通过添加命名空间范围的声明来使事情恢复正常...除了我们不能直接在我们的例子中,因为vector是一个类模板。事实上,当你第一次沉浸在课堂模板和朋友中时,你会遇到很多陷阱——例如,你可能会看到这个合理的常见问题解答项目并试图利用它,却发现它不适用于这种情况。

不是那么内联

如果你真的关心用户在不合格的电话之外命名advance(例如,获取地址或你有什么(,我的建议是将iteratorvector"分开":

// 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由于部分排序规则。部分排序几乎总是我的首选方法。