为什么我们有模板模板参数时还需要allocator::rebind?

34
每个分配器类必须具有类似以下接口的接口:
template<class T>
class allocator
{
    ...
    template<class Other>
    struct rebind { typedef allocator<Other> other; };
};

使用分配器的类会做一些冗余的操作,例如:

template<class T, class Alloc = std::allocator<T> >
class vector { ... };

但这为什么是必要的呢?
换句话说,他们不可以只说:
template<class T>
class allocator { ... };

template<class T, template<class> class Alloc = std::allocator>
class vector { ... };

他们为什么选择了导致更多冗余的“重新绑定”(即您必须两次使用T)的路线,而不是更优雅、不那么冗余,在某些类似情况下可能更安全的方案?(类似的问题也适用于char_traits和其他情况…尽管它们并非都有“重新绑定”,但它们仍然可以从模板模板参数中受益。)
编辑:
“但是,如果你需要超过1个模板参数,这种方法将行不通!”
实际上,这种方法非常有效!
template<unsigned int PoolSize>
struct pool
{
    template<class T>
    struct allocator
    {
        T pool[PoolSize];

        ...
    };
};

现在,如果vector只是这样定义的:
template<class T, template<class> class Alloc>
class vector { ... };

那么你可以直接说:
typedef vector<int, pool<1>::allocator> int_vector;

即使不需要您重复两次输入 int,它也可以完美地工作。

vector 中的 rebind 操作只需变为 Alloc<Other>,而不是 Alloc::template rebind<Other>::other


3
请注意,在C++11中,要求更加宽松,如果SomeAllocator没有提供rebind,则std::allocator_traits<SomeAllocator<T, Args...>>::rebind_alloc<U>默认为SomeAllocator<U, Args...>。这是一个合理的默认值。 - Luc Danton
1
到最后一点在编辑中:重新绑定操作在向量实现内部看起来多么丑陋是无关紧要的。你作为实现者,有责任让事情对用户变得简单,即使这意味着在底层使用非常丑陋和复杂的代码。如果你能将丑陋的部分隐藏在实现中以留下更清晰的接口,那就是你的工作。 - Mikael Persson
@MikaelPersson:当然,但对于用户来说真的更容易吗?(怎么样?举例或比较会很有帮助!:D) - user541686
真相可能令人失望。模板重新绑定惯用语在旧编译器上可能更容易实现。我在新的STL代码中发现了模板模板参数传递。因此,实现者不是一般不喜欢模板模板参数。我个人喜欢模板模板参数的原因是,在仅进行语法分析后,接口级别已经可以看到特定的意图,即为内部私有通用使用传递一种策略 - Patrick Fromberg
如果需要将 pool<1>::allocator<char>::rebind<int>::other 更改为 pool<4>::allocator<int> - Jarod42
allocator本身具有多个模板参数且与您的pool<1>::allocator(_其中外部而不是内部类具有模板参数_)不同时,它将无法工作。如果附加的模板(类型)参数具有默认值并且用户使用默认值,则可以工作(_这引发了为什么首先存在附加的模板参数的问题_)。此外,模板参数可以是类型或非类型。对于非类型,它将无法工作,这对于分配器用例非常不方便(_例如,对齐、填充等_)。 - Matthias
4个回答

24

来自C++11算法基础,第1卷,第4章,第35页的引用文本:

template <typename T> 
struct allocator 
{  
   template <typename U>  
   using  rebind = allocator<U>; 
}; 

示例用法:

allocator<int>::rebind<char> x;
在《C++程序设计语言》第四版的第34.4.1节中,第998页上,讨论了默认分配器类中“经典”重新绑定成员函数的注释:
template<typename U>
     struct rebind { using other = allocator<U>;};

Bjarne Stroustrup写道:

奇特的重新绑定模板是一个古老的别名。它应该被:

template<typename U>
using other = allocator<U>;

然而,在C++支持这些别名之前,分配器已经被定义了。


2
顺便提一下,引用 Stroustrup 的话是值得肯定的,不过你的链接已经失效了。 - odinthenerd

13
但是为什么需要这样做呢?
如果你的分配器类有多个模板参数怎么办?
总的来说,通常不建议使用模板模板参数,而是使用普通的模板参数,即使在实例化时可能会有一些冗余。在许多情况下(但可能不适用于分配器),该参数可能并不总是一个类模板(例如,具有模板成员函数的普通类)。
在容器类的实现中,你可能会发现使用模板模板参数很方便,因为它简化了一些内部语法。然而,如果用户拥有一个多参数类模板作为分配器,但你要求用户提供一个单参数类模板作为分配器,那么实际上你将迫使他为几乎任何新的上下文创建一个包装器来使用该分配器。这不仅不可扩展,而且还可能变得非常不方便。此时,该解决方案远非你最初认为的“优雅且不冗余”的解决方案。假设你有一个具有两个参数的分配器,哪种方法对用户来说最容易?
std::vector<T, my_allocator<T,Arg2> > v1;

std::vector<T, my_allocator_wrapper<Arg2>::template type > v2;

你的实现方式基本上强制用户构造许多无用的东西(包装器、模板别名等),只是为了满足你的实现需求。要求自定义分配器类的作者提供一个嵌套的重新绑定模板(它只是一个简单的模板别名)比使用另一种方法需要更容易些。

如果您的分配器类有多个模板参数怎么办?template<class P1, class P2> struct my_allocator_with_args { template<class T> my_allocator { ... }; };... 现在您只需将 my_allocator_with_args<Foo, Bar>::my_allocator 作为模板模板参数传递即可。 - user541686
顺便说一句,这个答案是David的答案的复制。 (它也是其他人的答案的复制,但他很快就删除了它。) - user541686
谢谢,现在看看我的修改。;) 如果你仍然认为这是一个更糟糕的选择,能否请你编辑你的答案,包括“不可扩展”的情况以及当我们使用rebind时等效的“可扩展”版本是什么?我觉得我们中有人漏掉了什么。 :) - user541686
顺便说一句,在您想出一个例子后,还有另一个问题:如果这不是模板参数旨在解决的问题类型,那么它们的目的是什么? - user541686

5
在您的方法中,您强制分配器成为具有单个参数的模板,这可能并不总是正确的。在许多情况下,分配器可以是非模板的,并且嵌套的rebind可以返回相同类型的分配器。在其他情况下,分配器可以有额外的模板参数。第二种情况是std :: allocator <>的情况,与标准库中的所有模板一样,只要实现提供默认值,就允许具有额外的模板参数。还要注意,在某些情况下,rebind的存在是可选的,可以使用allocator_traits来获取重新绑定的类型。
标准实际上提到,嵌套的rebind实际上只是一个模板化的typedef:

就我所知,模板参数的数量不可能是一个问题——如果你需要不同数量的参数,那么你只需要创建一个只需要1个参数的包装器,这非常容易且比使用rebind更安全/更少冗余,对吧? - user541686
@Mehrdad:你需要创建一个包装器,实际上更像是N个包装器,可能有很多,每个包装器对应程序中所有其他参数的可能组合,其中在每个包装器中,所有其他参数都将被绑定到某个值。 - David Rodríguez - dribeas
@Mehrdad:假设有一个模板化的分配器,它接受两个参数TArg,你如何创建一个单参数包装器,其参数为T,适用于所有可能的Arg值? - David Rodríguez - dribeas
嵌套模板类?外部类使用“Arg”,内部类使用“T”。您将内部类作为模板模板参数传递...实现与以前完全相同的目标,提供所需的模板参数数量,但仍按您需要的方式工作。template<class P1, class P2> struct my_allocator_with_args { template<class T> my_allocator { ... }; };...现在,您只需将my_allocator_with_args<Foo,Bar> :: my_allocator作为模板模板参数传递即可。我不知道N出现在哪里。 - user541686
(顺便说一句,“N”是因为没有对解决方案进行太多思考,微不足道的实现将是一个绑定的单参数模板:) - David Rodríguez - dribeas
显示剩余11条评论

0
假设您想编写一个函数,可以处理各种类型的向量。
那么,能够编写以下代码会更加方便:
template <class T, class A>
void f (std::vector <T, A> vec) {
   // ...
}

比起写

template <class T, template <class> class A>
void f (std::vector <T, A> vec) {
   // ...
}

在大多数情况下,这样的函数无论如何都不关心分配器。
此外,请注意,分配器不需要是一个模板。您可以为需要分配的特定类型编写单独的类。
设计分配器的更方便的方法可能是:
struct MyAllocator { 
   template <class T>
   class Core {
      // allocator code here
   };
};

那么就有可能编写如下代码:

std::vector <int, MyAllocator> vec;

与其使用有些误导性的表达方式

std::vector <int, MyAllocator<int> > vec;

我不确定在添加了rebind之后,上述的MyAllocator是否被允许作为一个分配器来使用,也就是说,以下是否是一个有效的分配器类:

struct MyAllocator { 
   template <class T>
   class Core {
      // allocator code here
   };

   template <class T>
   struct rebind { using other=Core<T>; };
};

JhonB,为什么不直接使用using other = core<T>而不是使用结构体? - RaGa__M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接