我想知道包装C ++ STL容器以保持一致性并且能够在不修改客户端代码的情况下交换实现是一个好主意。
例如,在一个项目中,我们使用CamelCase来命名类和成员函数(Foo::DoSomething()
),我会将std::list
包装成这样的类:
template<typename T>
class List
{
public:
typedef std::list<T>::iterator Iterator;
typedef std::list<T>::const_iterator ConstIterator;
// more typedefs for other types.
List() {}
List(const List& rhs) : _list(rhs._list) {}
List& operator=(const List& rhs)
{
_list = rhs._list;
}
T& Front()
{
return _list.front();
}
const T& Front() const
{
return _list.front();
}
void PushFront(const T& x)
{
_list.push_front(x);
}
void PopFront()
{
_list.pop_front();
}
// replace all other member function of std::list.
private:
std::list<T> _list;
};
然后我就能写出这样的东西:
typedef uint32_t U32;
List<U32> l;
l.PushBack(5);
l.PushBack(4);
l.PopBack();
l.PushBack(7);
for (List<U32>::Iterator it = l.Begin(); it != l.End(); ++it) {
std::cout << *it << std::endl;
}
// ...
我相信大多数现代C ++编译器都可以轻松优化额外的间接,我认为这种方法有一些优点,如:
我可以轻松扩展List类的功能。例如,我想要一个对列表进行排序然后调用unique()
的速记函数,我可以通过添加成员函数来扩展它:
template<typename T>
void List<T>::SortUnique()
{
_list.sort();
_list.unique();
}
此外,只要行为相同,我就可以交换底层实现(如果需要),而不会对它们使用的代码List<T>
进行任何更改。还有其他好处,因为它维护了项目中命名约定的一致性,因此对于STL没有push_back()
,对于整个项目中的其他类没有PushBack()
,如:
std::list<MyObject> objects;
// insert some MyObject's.
while ( !objects.empty() ) {
objects.front().DoSomething();
objects.pop_front();
// Notice the inconsistency of naming conventions above.
}
// ...
我想知道这种方法是否有任何重大(或次要)缺点,或者这是否真的是一种实用方法。
好的,谢谢你到目前为止的答案。我想我可能在问题的命名一致性方面投入太多。实际上命名约定不是我关注的,因为人们可以提供完全相同的接口:
template<typename T>
void List<T>::pop_back()
{
_list.pop_back();
}
或者甚至可以使另一个实现的接口看起来更像大多数C ++程序员已经熟悉的STL。但无论如何,在我看来,这更像是一种风格而且根本不重要。
我担心的是能够轻松更改实施细节的一致性。堆栈可以以各种方式实现:阵列和顶部索引,链表或甚至两者的混合,并且它们都具有数据结构的LIFO特性。自平衡二进制搜索树也可以用AVL树或红黑树实现,它们的搜索,插入和删除都具有O(logn)
平均时间复杂度。
因此,如果我有一个AVL树库和另一个具有不同接口的红黑树库,我使用AVL树来存储一些对象。后来,我想(使用分析器或其他)使用红黑树会提高性能,我将不得不去使用AVL树的文件的每个部分,并更改类,方法名称和可能的参数订购其红黑树同行。甚至有些情况下新类没有编写等效的功能。我认为它也可能因为实施方面的差异或者我犯了错误而引入了微妙的错误。
所以我开始想知道如果维护这样一个包装类来隐藏实现细节并为不同的实现提供统一的接口是值得的开销:
template<typename T>
class AVLTree
{
// ...
Iterator Find(const T& val)
{
// Suppose the find function takes the value to be searched and an iterator
// where the search begins. It returns end() if val cannot be found.
return _avltree.find(val, _avltree.begin());
}
};
template<typename T>
class RBTree
{
// ...
Iterator Find(const T& val)
{
// Suppose the red-black tree implementation does not support a find function,
// so you have to iterate through all elements.
// It would be a poor tree anyway in my opinion, it's just an example.
auto it = _rbtree.begin(); // The iterator will iterate over the tree
// in an ordered manner.
while (it != _rbtree.end() && *it < val) {
++it;
}
if (*++it == val) {
return it;
} else {
return _rbtree.end();
}
}
};
现在,我必须确保AVLTree::Find()
和RBTree::Find()
完全相同(即获取要搜索的值,将迭代器返回到元素或End()
,除此以外)。然后,如果我想从AVL树变为红黑树,我所要做的就是更改声明:
AVLTree<MyObject> objectTree;
AVLTree<MyObject>::Iterator it;
为:
RBTree<MyObject> objectTree;
RBTree<MyObject>::Iterator it;
通过维护两个类,其他一切都是一样的。
答案 0 :(得分:6)
我想知道这种方法是否有任何重大(或次要)缺点,
两个字:维护噩梦。
然后,当你得到一个新的支持移动的C ++ 0x编译器时,你将不得不扩展所有的包装类。
不要误解我的意思 - 如果你需要额外的功能,包装STL容器没有错,只是为了“一致的成员函数名称”?开销太大了。投入太多时间没有投资回报率。
我应该补充一点:在使用C ++时,不一致的命名约定只是你的习惯。在太多可用(和有用)的库中存在太多不同的样式。
答案 1 :(得分:3)
听起来像是typedef的工作,而不是包装器。
答案 2 :(得分:2)
...能够在不修改客户端代码的情况下交换实现
在解决问题的代码级别,您可能希望在呈现相同的API时选择不同的内部容器。但是,您无法合理地选择专门用于使用List的代码,然后将实现交换到其他任何内容,而不会影响导致客户端代码选择列表的性能和内存利用率特性。在标准容器中做出的妥协是很好理解和记录良好的...不值得贬低您的编程人员的教育,可用的参考资料,增加新员工加快速度的时间等,只是为了拥有一个自己开发的包装。
答案 3 :(得分:2)
任何体面的编译器都能够像标准类一样快速地创建包装类,但是在调试器和其他可能专用于标准容器的工具中,您的类可能不太可用。 此外,您可能会收到有关编译时错误的错误消息,这些错误比程序员在使用模板时出错时已经隐藏的更加神秘。
您的命名惯例对我来说并不比标准惯例更好;实际上IMO有点糟糕,有些危险;例如它对类和方法使用相同的CamelCasing,让我想知道你是否知道标准对_list
等名称施加的限制......
此外,您的命名约定只有您自己和其他一些人知道,而标准的命名约定是由大量C ++程序员(包括您和希望其他少数人)知道的。那些你可能需要添加到项目中的外部库呢?您是否要更改其界面,以便使用包装的自定义容器而不是标准容器?
所以我想知道,使用自定义命名约定的优势在哪里?我只看到了缺点......
答案 4 :(得分:1)
包装STL容器以使其具有线程安全性,隐藏用户实现细节,为用户提供有限的功能......这些都是有效的原因。
因为它不符合你的套管惯例而包装一个是浪费时间和金钱,并可能带来错误。只需接受STL使用全小写。
答案 5 :(得分:0)
不要不像那样包裹它们 如果要更改容器,请包装容器 例如,如果你有一个unordered_map,它应该只在内部添加/删除元素,但是应该公开[]运算符你将它包装起来并创建你自己的[]暴露内部容器的[]你还暴露了一个const_iterator,它是unordered_map的常量性。
答案 6 :(得分:0)
回复您的修改:我建议您查看适配器设计模式:
将类的接口转换为 客户期望的其他接口。 适配器允许类一起工作 否则,因为 不兼容的接口。 (设计 模式,E。Gamma等人。)
然后,您应该改变您的观点并使用不同的词汇表:将您的类视为翻译器而不是包装器,并且更喜欢术语“适配器”。
您将拥有一个抽象基类,用于定义应用程序所需的公共接口,以及每个特定实现的新子类:
class ContainerInterface
{
virtual Iterator Find(...) = 0;
};
class AVLAdapter : public ContainerInterface
{
virtual Iterator Find(...) { /* implement AVL */ }
}
class RBAdapter : public ContainerInterface
{
virtual Iterator Find(...) { /* implement RB tree */ }
}
您可以轻松地在可能的实施之间进行交换:
ContainerInterface *c = new AVLAdapter ;
c->Find(...);
delete c;
c = new RBAdapter ;
c->Find(...);
这种模式是可扩展的:测试新的实现,提供一个新的子类。应用程序代码不会更改。
class NewAdapter : public ContainerInterface
{
virtual Iterator Find(...) { /* implement new method */ }
}
delete c;
c = new NewAdapter ;
c->Find(...);