为什么大多数STL实现中的代码如此复杂?


71

STL是C ++世界的重要组成部分,大多数实现源自Stepanov和Musser的初步努力。

我的问题是代码的关键性,它是人们出于敬畏和学习目的而查看编写良好的C ++示例的主要来源之一:为什么STL的各种实现如此令人讨厌地看待-令人费解的和从美学的角度讲,通常是很好的例子,说明如何不编写C ++代码。

下面的代码示例由于在变量命名,布局,宏以及使用运算符等方面的原因而无法在我工作过的地方通过代码审查,这些操作不仅仅需要一目了然才能弄清实际情况。

template<class _BidIt> inline
bool _Next_permutation(_BidIt _First, _BidIt _Last)
{  // permute and test for pure ascending, using operator<
_BidIt _Next = _Last;
if (_First == _Last || _First == --_Next)
   return (false);

for (; ; )
   {  // find rightmost element smaller than successor
   _BidIt _Next1 = _Next;
   if (_DEBUG_LT(*--_Next, *_Next1))
      {  // swap with rightmost element that's smaller, flip suffix
      _BidIt _Mid = _Last;
      for (; !_DEBUG_LT(*_Next, *--_Mid); )
         ;
      _STD iter_swap(_Next, _Mid);
      _STD reverse(_Next1, _Last);
      return (true);
      }

   if (_Next == _First)
      {  // pure descending, flip all
      _STD reverse(_First, _Last);
      return (false);
      }
   }
}


_Ty operator()()
   {  // return next value
   static _Ty _Zero = 0;   // to quiet diagnostics
   _Ty _Divisor = (_Ty)_Mx;

   _Prev = _Mx ? ((_Ity)_Ax * _Prev + (_Ty)_Cx) % _Divisor
      : ((_Ity)_Ax * _Prev + (_Ty)_Cx);
   if (_Prev < _Zero)
      _Prev += (_Ty)_Mx;
   return (_Prev);
   }

请注意,我不会批评该界面,因为它的设计和适用性都很好。我担心的是实现细节的可读性。

先前已经提出过类似的问题:

是否有可读的STL实现

为什么STL的实现如此难以理解?如何在这里改善C ++?

注意:上面提供的代码来自MSVC 2010算法和队列头。


2
STL并不是其他语言中广泛采用的对象模型。但这不是您抱怨的重点。当语言具有预处理器时,这是库设计人员必须经历的那种废话。哦,而且必须在标头中编写所有实现。
汉斯·帕桑

20
很难写。应该很难读。
输入正确的意见,2010年

6
@Hans:他们已经说了二十年了,好了,我们正在从头做起。”-因为c ++ 0x需要对move和rvalue语义进行更改,所以现在不是说让我们重新创建的好时机。 -写入整个事情,只用语言,使用所有被添加新的好东西等等

5
“鉴于……它是人们出于敬畏和学习目的而查看编写良好的C ++示例的主要来源之一”虽然使用STL可以帮助程序员编写更简单甚至更清晰的代码,但您为什么认为它曾经是为STL的实现细节要简单明了,并且可以作为C ++的一个很好的例子?
TheUndeadFish

4
@sonicoder:无疑有人拥有此代码的版权。至少请确定从何处复制代码。
James McNellis

Answers:


20

关于变量名称,库实现者必须使用“疯狂的”命名约定,例如以下划线开头,后跟大写字母的名称,因为此类名称是为它们保留的。它们不能使用“普通”名称,因为这些名称可能已由用户宏重新定义。

第17.6.3.3.2节“全局名称”§1规定:

某些名称和函数签名集始终保留给实现:

  • 包含双下划线或以下划线后跟大写字母开头的每个名称都保留给实现以供任何使用。

  • 每个以下划线开头的名称都保留给实现,以用作全局名称空间中的名称。

(请注意,这些规则禁止类似__MY_FILE_H我经常见到的标题保护。)


@peoro,名称空间对类中的变量没有帮助。
温斯顿·埃韦特2010年

6
@peoro命名空间不能保护您免受预处理器宏的侵害。_Names是为实现保留的,所以如果您有这样的宏,它就在您的头上。另一方面,您不应该被迫猜测STL实现可能决定内部使用标识符的原因,而您不应该创建一个宏,尤其是在不同的实现之间,它可能会有所不同。
Logan Capaldo

你是对的。我跳过了“宏”,然后想到了用户变量/函数,这没有任何意义。
peoro

19

尼尔·巴特沃思(Neil Butterworth)现在被列为“匿名”,在他对SO问题“ STL的可读实现吗?”的回答中提供了有用的链接。。在那里引用他的答案:

由原始STL设计师Stepanov&Lee(与PJ Plauger和David Musser共同)合着的《 C ++标准模板库》一书描述了可能的实现,并附有代码-请参见 http://www.amazon。 co.uk/C-Standard-Template-Library/dp/0134376331

另请参见该线程中的其他答案。

无论如何,大多数STL代码(在STL中,我的意思是C ++标准库的类似STL的子集)都是模板代码,因此必须是仅标头的,并且由于几乎所有程序都使用了它,因此值得付出代码越短越好。

因此,简洁性和可读性之间的自然取舍点在量表的简洁性端比“普通”代码要远得多。

另外,在标准库中,应用程序代码的与系统无关的视图与底层系统连接在一起,并利用您作为应用程序开发人员应尽量避免的各种特定于编译器的事物。


1
简洁是一个方面,性能是另一方面(也可能损害可读性):人们总是抱怨STL实施中存在可疑的脂肪和效率低下-多年来,几乎所有内容都被减少了,但是偶尔会在可读性方面进行取舍和优雅。
托尼·德罗伊

12

变量名是因为它是标准库代码,因此应使用保留名称作为标头中的实现详细信息。以下应打破标准库:

#define mid
#include <algorithm>

因此,标准库标头不能mid用作变量名_Mid。STL是不同的-它不是语言规范的一部分,它被定义为“这里有一些标头,请按需使用”

另一方面,如果将您的代码或我的代码用作_Mid变量名,则该代码或我的代码将是无效的,因为这是保留名称-允许执行该操作:

#define _Mid

如果感觉像这样。

布局-嗯。他们可能有一个样式指南,他们或多或少会遵循它。它们与我的样式指南不匹配(因此将使我的代码审查失败)这一事实对他们而言并不重要。

难以解决的操作员-对谁难?应该为维护它的人员编写代码,而GNU / Dinkumware //可能不想让人们松开*--_Next一眼就无法理解的标准库的代码。如果您使用这种表达方式,就会习惯它,否则,您会继续感到困难。

不过,我会给您的是,这种operator()超载是乱码。[编辑:我明白了,它是线性同余生成器,非常通用,如果模数为“ 0”,则意味着仅使用算术类型的自然环绕。


2
但这确实会破坏标准库。他们如何定义std::pair何时first变为零?
彼得·亚历山大

@Peter:天哪!我会想一个更好的。
史蒂夫·杰索普

2
Aaah我明白了,所以它是一种反盗版/反规避技术... :)

@sonicoder:是的,或者如果您读过“城市与城市”,“无处不在”或“哈利·波特”,那么向导和麻瓜可以共存而不会撞到彼此。
史蒂夫·杰索普

1
FWIW,RMS或多或少地发明了(或至少大大地开发了)开源软件的精神,而且我敢肯定,glibc中使用了这种习惯用法。如果GNU认为*--合理的运算符组合而您却没有,那么大多数人都将遵循GNU,因此将能够阅读它。我个人并不那么在意:当然,我不认为代码应该是晦涩难懂的,但是同样地,我也不认为应该使用像C ++这样棘手的语言来放置与标准库同样重要的组件放弃发明和使用成语只是因为初学者一开始就觉得很难。
史蒂夫·杰索普

4

实现方式各不相同。 例如,libc ++在眼睛上要容易得多。虽然仍然有一些下划线的噪音。正如其他人指出的那样,不幸的是需要下划线。这是libc ++中的相同函数:

template <class _Compare, class _BidirectionalIterator>
bool
__next_permutation(_BidirectionalIterator __first, _BidirectionalIterator __last, _Compare __comp)
{
    _BidirectionalIterator __i = __last;
    if (__first == __last || __first == --__i)
        return false;
    while (true)
    {
        _BidirectionalIterator __ip1 = __i;
        if (__comp(*--__i, *__ip1))
        {
            _BidirectionalIterator __j = __last;
            while (!__comp(*__i, *--__j))
                ;
            swap(*__i, *__j);
            _STD::reverse(__ip1, __last);
            return true;
        }
        if (__i == __first)
        {
            _STD::reverse(__first, __last);
            return false;
        }
    }
}

3
只有编译器特定的关键字和宏才需要它们,而不是类/结构成员。尽管约定是在其他前缀之前,并且“ M_”或“ m_”或附加“ _”来表示类成员,而不是局部变量或全局变量。

好吧,我想这里还有改进的余地:)
ergosys 2010年

1
不过,这段代码确实与原始问题中的代码没有什么不同。它只是重新格式化缩进,while而不是for用于循环,并切出调试支持宏。所有这些都属于“琐碎的编码标准问题”类别,并且都不是特别重要的(嗯,除调试支持宏外,但是,如果要调试支持,则需要编写两个实现或使用宏来有条件地进行编译它)。
James McNellis

1
我坚持认为,它更易于阅读,而我的例子是,存在更多易读的实现。我对OP的示例感到头疼,但是这个还不错。
ergosys

6
@sonicoder:不,由于@Steve Jessop和其他人提到的原因,所有内容都必须带有下划线:理论上,任何非保留名称都可以由库用户重新定义为宏,这将破坏标准库代码。因此,它们必须使用前划线,因为这些是为实现保留的名称。
jalf

2

我怀疑部分原因是STL中的代码已高度优化。正在实现的代码种类的性能比可读性要重要得多。因为它们被广泛使用,所以使它们尽可能快是有意义的。


6
您可以编写真正的最佳代码,而不必费解,STL的存在只是20年前开始的库的变体。

0

加上人们已经说过的话,您看到的样式就是GNU样式。丑陋?也许,这是情人眼中的。但这是严格定义的样式,它确实使所有代码看起来都相似,而不是难以适应。


不,那不是GNU风格。如后面对问题的显示所示,代码来自Microsoft std :: lib。
乔纳森·韦克利
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.