C++中的内部typedef - 是好风格还是坏风格?

200

最近我经常发现自己在类中声明与其相关的typedef,即:

class Lorem
{
    typedef boost::shared_ptr<Lorem> ptr;
    typedef std::vector<Lorem::ptr>  vector;

//
// ...
//
};

这些类型随后在代码中其他地方被使用:

Lorem::vector lorems;
Lorem::ptr    lorem( new Lorem() );

lorems.push_back( lorem );

我喜欢的原因:

  • 它减少了类模板引入的噪音,例如std::vector<Lorem>变为Lorem::vector
  • 它作为一种意图的陈述 - 在上面的例子中,Lorem类打算通过boost::shared_ptr进行引用计数并存储在向量中。
  • 它允许实现更改 - 例如,如果后来需要将Lorem更改为通过boost::intrusive_ptr进行引用计数,则对代码的影响很小。
  • 我认为它看起来更美观,阅读起来也更容易。

我不喜欢的原因:

  • 有时候会存在依赖问题 - 如果您想在另一个类中嵌入例如Lorem::vector,但只需要(或希望)前置声明Lorem(而不是引入其头文件),那么您最终会使用显式类型(例如boost::shared_ptr<Lorem>而不是Lorem::ptr),这有点不一致。
  • 它可能不太常见,因此可能更难理解?

我尽力以客观的编码风格进行,所以能得到其他人的意见会很有帮助,这样我就可以分析思维了。

9个回答

177

我认为这是一种优秀的风格,我自己也使用它。尽可能地限制名称的作用域,并且在C++中使用类是最好的方法。例如,C++标准库在类中大量使用typedef。


这是一个很好的观点,我想知道是否“更漂亮”是我的潜意识在微妙地指出有限的范围是一件好事。不过我想知道,STL主要在类模板中使用它,这是否使其用法略有不同?在“具体”的类中是否更难以证明其合理性? - Will Baker
1
标准库由模板而非类组成,但我认为两者的理由是相同的。 - anon

13
它作为一种意图的陈述 - 在上面的示例中,Lorem类 旨在通过boost :: shared_ptr进行引用计数 并存储在向量中。
这正是它做的事情。
如果我在代码中看到'Foo :: Ptr',我绝对不知道它是shared_ptr还是Foo *(STL有:: pointer typedefs,即T *,请记住)或其他任何东西。特别是如果它是共享指针,我根本不提供typedef,而是在代码中显式地保留shared_ptr使用。
实际上,除了模板元编程外,我几乎从不使用typedef。
STL经常做这种事情
STL设计中使用以成员函数和嵌套typedef定义的概念是历史性的死胡同,现代模板库使用自由函数和特征类(参见Boost.Graph),因为这些不会将内置类型排除在概念建模之外,并且使得适应未考虑给定模板库概念的类型更容易。
不要把STL用作犯同样错误的原因。

1
我同意你的第一部分,但是你最近的编辑有点短视。这种嵌套类型简化了特质类的定义,因为它们提供了一个合理的默认值。考虑新的std::allocator_traits<Alloc>类...你不必为每个单独编写的分配器专门指定它,因为它直接从Alloc借用类型即可。 - Dennis Zickefoose
@Dennis:在C++中,方便应该在库的/用户/一侧,而不是在其/作者/一侧:用户希望获得特征的统一接口,只有特征类才能做到这一点,因为上面所述的原因。但即使作为“Alloc”作者,为其新类型专门化std::allocator_traits<>也并不比添加所需的typedefs更难。我还编辑了答案,因为我的完整回复无法适应评论。 - Marc Mutz - mmutz
但它确实站在用户的一边。作为尝试创建自定义分配器的allocator_traits用户,我不必费心处理traits类的十五个成员...我只需要说typedef Blah value_type;并提供适当的成员函数,而默认的allocator_traits将解决其余问题。此外,请看看您的Boost.Graph示例。是的,它大量使用特性类...但是graph_traits<G>的默认实现仅查询G的内部typedefs。 - Dennis Zickefoose
1
即使是03标准库在适当的情况下也会使用特性类...库的哲学不是通用地操作容器,而是操作迭代器。因此,它提供了一个iterator_traits类,以便您的通用算法可以轻松查询适当的信息。再次强调,默认情况下查询迭代器自身的信息。长话短说,特性和内部typedef并不互相排斥...它们相互支持。 - Dennis Zickefoose
1
@Dennis:iterator_traits 的出现是因为 T* 应该是 RandomAccessIterator 的模型,但你不能将所需的 typedefs 放入 T* 中。一旦有了 iterator_traits,嵌套的 typedefs 就变得多余了,我希望它们当时就被删除了。出于同样的原因(无法添加内部 typedefs),T[N] 不符合 STL 的 Sequence 概念,你需要使用诸如 std::array<T,N> 的 hack。Boost.Range 展示了如何定义一个现代 Sequence 概念,使 T[N] 可以成为其模型,因为它不需要嵌套的 typedefs 或成员函数。 - Marc Mutz - mmutz
显示剩余2条评论

9

7

类型定义绝对是良好的编程风格。你提到的“我喜欢的理由”都是正确的。

关于你提到的问题,前向声明并不是万能的。你可以简单地设计你的代码以避免多级依赖。

你可以将typedef移出类,但Class::ptr比ClassPtr更美观,所以我不这样做。对我来说,这就像命名空间一样 - 在作用域内保持联系。

有时我会这样做:

Trait<Loren>::ptr
Trait<Loren>::collection
Trait<Loren>::map

它可以作为所有域类的默认设置,并针对某些特定的域类进行一些特殊设置。


3
STL经常这样做——在STL的许多类中,typedef是接口的一部分。
reference
iterator
size_type
value_type
etc...

这里的typedef都是各种STL模板类接口的一部分。


真的,我怀疑这是我第一次学到它的地方。虽然这些似乎更容易被证明?如果你沿着“元编程”的思路思考,我不禁认为类模板中的typedef更类似于变量。 - Will Baker

3

我目前正在处理大量使用这些typedef的代码,这很好。

但是,我注意到经常会出现迭代的typedef,其定义分散在几个类中,你永远不知道你正在处理哪种类型。我的任务是总结一些复杂数据结构的大小,这些结构隐藏在这些typedef后面 - 因此我不能依赖现有接口。再加上三到六层嵌套的命名空间,就会变得混乱不堪。

所以,在使用它们之前,需要考虑以下几点:

  • 是否有其他人需要这些typedef?该类被其他类广泛使用吗?
  • 我是否缩短了使用或隐藏了类?(如果隐藏了,也可以考虑接口。)
  • 其他人是否在使用这段代码?他们是如何做的?他们会认为这更容易还是会感到困惑?

3

当typedef仅在类内部使用时(即被声明为私有),我认为这是个好主意。然而,出于你提到的原因,如果typedef需要在类外被知晓,我不会使用它。在这种情况下,我建议将它们移动到类外。


3
另外一种看法认为这是个好主意。当我在编写一个需要高效的模拟器时,包括时间和空间效率,我开始使用值类型的 Ptr 类型定义,它最初是 Boost 共享指针。然后我进行了一些分析并将其中一些更改为 Boost 内部指针,而不需要更改使用这些对象的任何代码。
请注意,仅在您知道类将在哪里使用且所有用途具有相同要求时才有效。例如,我不会在库代码中使用此方法,因为编写库时无法预知其将在何种上下文中使用。

1
我建议将那些typedef移到类外面。这样,您就可以仅在需要时包含它们,从而消除对共享指针和向量类的直接依赖。除非您在类实现中使用这些类型,否则我认为它们不应该是内部typedef。
您喜欢它的原因仍然成立,因为通过typedef进行类型别名解决了它们,而不是在类内部声明它们。

那样做会污染匿名命名空间的typedefs是吧?!typedef的问题在于它隐藏了实际类型,在多个模块中引入时可能会导致冲突,这些冲突很难找到和解决。将它们放在命名空间或类内部是一个好习惯。 - Indy9000
3
名称冲突和匿名命名空间污染与将类型名放在类内或类外没有太大关系。您的类可能会与其他名称发生冲突,但是typedef不会。因此,为避免名称污染,请使用命名空间,在其中声明类及相关的typedef。 - Cătălin Pitiș
1
将typedef放在类内部的另一个论点是使用模板化函数。例如,当函数接收未知容器类型(vector或list)包含未知字符串类型(string或您自己的字符串兼容变体)时,唯一确定容器有效负载类型的方法是使用容器类定义中的typedef“value_type”。 - Marius

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