包装容器以保持一致性
我想知道包装 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
的代码。还有其他好处,因为它保持了项目中命名约定的一致性,因此它没有用于 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;
通过维护两个类,其他一切都将相同。
I'm wondering if it's a good idea to wrap C++ STL containers to maintain consistency and being able to swap the implementation without modifying client code.
For example, in a project we use CamelCase for naming classes and member functions (Foo::DoSomething()
), I would wrap std::list
into a class like this:
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;
};
Then I would be able to write something like this:
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;
}
// ...
I believe most of the modern C++ compliers can optimize away the extra indirection easily, and I think this method has some advantages like:
I can extend the functionality of the List class easily. For instance, I want a shorthand function that sorts the list and then call unique()
, I can extend it by adding a member function:
template<typename T>
void List<T>::SortUnique()
{
_list.sort();
_list.unique();
}
Also, I can swap the underlying implementation (if needed) without any change on the code they uses List<T>
as long as the behavior is the same. There are also other benefits because it maintains consistency of naming conventions in a project, so it doesn't have push_back()
for STL and PushBack()
for other classes all over the project like:
std::list<MyObject> objects;
// insert some MyObject's.
while ( !objects.empty() ) {
objects.front().DoSomething();
objects.pop_front();
// Notice the inconsistency of naming conventions above.
}
// ...
I'm wondering if this approach has any major (or minor) disadvantages, or if this is actually a practical method.
Okay, thanks for the answers so far. I think I may have put too much on naming consistency in the question. Actually naming conventions are not my concern here, since one can provide an exactly same interface as well:
template<typename T>
void List<T>::pop_back()
{
_list.pop_back();
}
Or one can even make the interface of another implementation look more like the STL one that most C++ programmers are already familiar with. But anyway, in my opinion that's more of a style thing and not that important at all.
What I was concerned is the consistency to be able to change the implementation details easily. A stack can be implemented in various ways: an array and a top index, a linked list or even a hybrid of both, and they all have the LIFO characteristic of a data structure. A self-balancing binary search tree can be implemented with an AVL tree or a red-black tree also, and they both have O(logn)
average time complexity for searching, inserting and deleting.
So if I have an AVL tree library and another red-black tree library with different interfaces, and I use an AVL tree to store some objects. Later, I figured (using profilers or whatever) that using a red-black tree would give a boost in performance, I would have to go to every part of the files that use AVL trees, and change the class, method names and probably argument orders to its red-black tree counterparts. There are probably even some scenarios that the new class do not have an equivalent functionality written yet. I think it may also introduce subtle bugs also because of the differences in implementation, or that I make a mistake.
So what I started to wonder that if it is worth the overhead to maintain such a wrapper class to hide the implementation details and provide an uniform interface for different implementations:
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();
}
}
};
Now, I just have to make sure that AVLTree::Find()
and RBTree::Find()
does exactly the same thing (i.e. take the value to be searched, return an iterator to the element or End()
, otherwise). And then, if I want to change from an AVL tree to a red-black tree, all I have to do is change the declaration:
AVLTree<MyObject> objectTree;
AVLTree<MyObject>::Iterator it;
to:
RBTree<MyObject> objectTree;
RBTree<MyObject>::Iterator it;
and everything else will be the same, by maintaining two classes.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
听起来像是 typedef 的工作,而不是包装器。
Sounds like a job for a typedef, not a wrapper.
在代码的问题解决级别,您可能希望在呈现相同的 API 时选择不同的内部容器。但是,您无法合理地使用专门选择使用列表的代码,然后将实现交换为其他任何内容,而不影响导致客户端代码首先选择列表的性能和内存利用率特征。标准容器中所做的妥协是很好理解的,并且有详细记录......仅仅为了拥有一个本土包装器而贬低编程人员的教育、可用的参考材料、增加新员工熟悉速度的时间等是不值得的。
At the problem-solving level of code, you may want to select a different internal container while presenting the same API. But, you can't reasonably have code that chooses specifically to use a List, then swap the implementation to anything else without compromising the performance and memory utilisation characteristics that led the client code to select a list to begin with. The compromises made in the Standard containers are well understood and well documented... it's not worth devaluing your programming staff's education, available reference material, increasing time for new staff to get up to speed etc. just to have a homegrown wrapper.
任何像样的编译器都能够使您的包装类与标准类一样快,但是您的类在调试器和其他专门用于标准容器的工具中可能不太可用。
此外,您可能会收到有关编译时错误的错误消息,这些错误消息比程序员在使用模板时犯错误时收到的已经很神秘的消息更加神秘。
在我看来,你的命名约定并不比标准命名约定更好;在我看来,这实际上更糟糕,而且有点危险;例如,它对类和方法使用相同的 CamelCasing,这让我想知道您是否知道标准对像
_list
这样的名称施加的限制......此外,您的命名约定只有您自己知道可能还有其他一些,相反,标准的程序员被大量 C++ 程序员(包括您,希望包括其他少数人)所熟知。那么您可能需要添加到项目中的外部库又如何呢?您是否要更改他们的界面,以便使用包装的自定义容器而不是标准容器?
所以我想知道,使用自定义命名约定的优点在哪里?我只看到缺点...
Any decent compiler will be able to make your wrapped classes as fast as the standard ones, however your classes may be will be less usable in debuggers and other tools that could have been specialized for standard containers.
Also you are probably going to have error messages about compile time errors that are a bit more cryptic than the already cryptic ones that a programmer gets when making mistakes in the use of a template.
Your naming convention doesn't look to me any better than the standard one; it's actually IMO a bit worse and somewhat dangerous; e.g. it uses the same CamelCasing for both classes and methods and makes me wonder if you know what are the limitations imposed by the standard on a name like
_list
...Moreover your naming convention is known only by you and probably a few others, instead the standard one is known by a huge number of C++ programmers (including you and hopefully those few others). And what about external libraries you may need to add to your project? Are you going to change their interface so that your wrapped custom containers are used instead of standard ones?
So I wonder, where are the pluses of using your custom naming convention? I only see downsides...
包装 STL 容器以使其线程安全、隐藏用户实现细节、为用户提供有限的功能……这些都是正当理由。
仅仅因为它不遵循您的外壳约定而对其进行包装是浪费时间和金钱,并且可能会带来错误。只要接受 STL 使用全小写即可。
Wrapping STL containers to make them thread-safe, to hide away from the user implementation detail, to give the user limited functionality... these are valid reasons.
Wrapping one just because it does not follow your casing convention is a waste of time and money and could bring in bugs. Just accept that STL uses all-lowercase.
不,不要那样包裹它们。
如果您想更改容器的内容,请包装容器。
例如,如果您有一个 unordered_map ,它应该只在内部添加/删除元素,但应该公开 [] 运算符,您将其包装起来并创建自己的 [] 来公开内部容器的 [] ,并且您还公开一个 const_iterator ,它是 unordered_map 的常量迭代器。
No don't wrap them like that.
You wrap containers if you want to change what the containers are.
For example, if you have an unordered_map that should only add/remove elements internally but should expose the [] operator you wrap it up and create your own [] that exposes the internal container's [] and you also expose a const_iterator that is the unordered_map's const_iterator.
回复您的编辑:我建议您查看适配器设计模式:
然后,您应该改变您的观点并使用不同的词汇:将您的类视为一个翻译器而不是包装器,并且更喜欢术语“适配器”。
您将拥有一个抽象基类,它定义应用程序所需的通用接口,以及每个特定实现的新子类:
您可以轻松地在可能的实现之间进行交换:
并且此模式是可扩展的:要测试新的实现,请提供新的实现子类。应用程序代码不变。
In reply to your edit: I suggest you have a look at the Adapter Design Pattern:
Then, you should change your point of view and use a different vocabulary: consider your class as a translator instead of a wrapper and and prefer the term "adapter".
You will have an abstract base class that defines a common interface expected by your application, and a new subclass for each specific implementation:
You can easily swap between the possible implementations:
And this pattern is scalable: to test a new implementation, provide a new subclass. The application code does not change.
两个词:维护噩梦。
然后,当您获得新的支持移动的 C++0x 编译器时,您将必须扩展所有包装类。
不要误会我的意思——如果您需要额外的功能,包装 STL 容器并没有什么问题,但只是为了“一致的成员函数名称”?开销太大。投入太多时间却没有投资回报。
我应该补充一点:在使用 C++ 时,不一致的命名约定是您必须面对的问题。太多可用(和有用)的库中有太多不同的样式。
Two words: Maintenance nightmare.
And then, when you get a new move-enabled C++0x compiler, you will have to extend all your wrapper classes.
Don't get me wrong -- there's nothing wrong with wrapping an STL container if you need additional features, but just for "consistent member function names"? Too much overhead. Too much time invested for no ROI.
I should add: Inconsistent naming conventions is just something you live with when working with C++. There's just too much different styles in too much available (and useful) libraries.