【问题标题】:Generic/template programming best practices: To limit types, or not to limit types通用/模板编程最佳实践:限制类型,或不限制类型
【发布时间】:2011-08-28 05:14:39
【问题描述】:

这是我的问题。我只是好奇关于限制可以传递给通用函数或类的类型的共识是什么。我想我在某个时候读过,如果你在做泛型编程,通常最好让事情保持开放而不是试图关闭它们(不记得源代码)。

我正在编写一个具有一些内部通用函数的库,我认为它们应该只允许库中的类型与它们一起使用,因为这就是我要使用它们的意思。另一方面,我不确定我为锁定事物所做的努力是否值得。

有人可能有一些关于这个话题的统计资料或权威评论的来源吗?我也对合理的意见感兴趣。希望这不会使这个问题完全无效:\

此外,SO 上是否有任何等同于“最佳实践”的标签?我没有具体看到那个,但能够为给定的 SO 主题提供所有最佳实践信息似乎会有所帮助......也许不是,只是一个想法。

编辑:到目前为止,一个答案提到我正在做的图书馆类型将很重要。它是一个数据库库,最终与 STL 容器、可变参数(元组)、Boost Fusion 以及类似的东西一起工作。我可以看到这有什么关系,但我也对决定走哪条路的经验法则感兴趣。

【问题讨论】:

  • “如果你在做泛型编程,通常最好让事物保持开放而不是试图关闭它们”:这是非常值得商榷的。但是,由于 C++ 人认为概念在未来几年(几十年?)内不会成为该语言的一部分,因此与远程尝试关闭它们相比,让事物保持开放更方便
  • @Alexandre 我想知道什么时候会提到概念:(

标签: c++ templates boost c++11 generic-programming


【解决方案1】:

始终尽可能保持打开状态 - 但请确保

  • 记录所需的接口和有效类型的行为以与您的通用代码一起使用。
  • 使用类型的接口特征(特征)来确定是否允许/禁止它。不要根据类型名称做出决定。
  • 产生合理的诊断如果 有人使用了错误的类型。 C++ 模板非常适合提高吨位 如果它们被实例化,则深度嵌套的错误 错误类型 - 使用类型特征、静态断言和相关技术,可以轻松生成更简洁的错误消息。

【讨论】:

  • 很好的答案,但最后一点实施起来并不总是微不足道的。
  • @James: 使用 C++0x static_assert 会更容易。
  • 如果您不想依赖static_assert,也可以使用BOOST_MPL_ASSERT_MSG。此外,以下链接包含一些关于减少错误类型可能导致的错误数量的提示和技巧。 cpp-next.com/archive/2010/09/…
【解决方案2】:

在我的数据库框架中,我决定放弃模板并使用单个基类。通用编程意味着可以使用 anyall 对象。特定类型类超过了少数通用操作。例如,可以比较字符串和数字是否相等; BLOB(二进制大对象)可能想要使用不同的方法(例如比较存储在不同记录中的 MD5 校验和)。

此外,字符串和数字类型之间存在继承分支。

通过使用继承层次结构,我可以使用Field 类或Field_Int 等专用类来引用任何字段。

【讨论】:

  • 通用编程不仅仅是编译时,正如您明智地指出的那样。
【解决方案3】:

STL 最强大的卖点之一是它非常开放,它的算法适用于我的数据结构以及它自己提供的数据结构,我的算法适用于它的数据结构以及它的数据结构矿。

让您的算法对所有类型开放还是将它们限制为您自己的算法是否有意义在很大程度上取决于您正在编写的库,我们对此一无所知。

(最初我的意思是说,泛型编程的意义在于广泛开放,但现在我看到泛型总是有限制的,你必须在某个地方划清界限。它也可能仅限于你的类型,如果这有意义的话。)

【讨论】:

  • 我在底部发布了我的问题的编辑。这是一个数据库库。最终与 STL、元组、Boost Fusion 一起工作,那里有很多通用性:D 我的一个问题是,如何确定在哪里画这条线?
  • 这条评论让我想起了马尔科姆·格拉德威尔的大卫和歌利亚书,权力的章节限制:)。
【解决方案4】:

至少在 IMO,正确的做法大致是概念所尝试的:与其尝试验证您是否收到指定类型(或一组指定类型之一),不如尽最大努力指定要求类型,并验证您收到的类型是否具有正确的特征,并且可以满足您的模板要求。

与概念非常相似,这样做的主要动机是在未满足这些要求时提供良好、有用的错误消息。最终,如果有人试图通过不满足其要求的类型实例化您的模板,编译器产生错误消息。问题是,除非您采取措施确保它确实有用,否则该错误消息将不会很有帮助。

【讨论】:

    【解决方案5】:

    问题

    如果您的客户可以在公共标头中看到您的内部函数,并且这些内部泛型函数的名称是“常见的”,那么您可能会让您的客户面临意外调用您的内部泛型函数的风险。

    例如:

    namespace Database
    {
    
    // internal API, not documented
    template <class DatabaseItem>
    void
    store(DatabaseItem);
    {
        // ...
    }
    
    struct SomeDataBaseType {};
    
    }  // Database
    
    namespace ClientCode
    {
    
    template <class T, class U>
    struct base
    {
    };
    
    // external API, documented
    template <class T, class U>
    void
    store(base<T, U>)
    {
        // ...
    }
    
    template <class T, class U>
    struct derived
        : public base<T, U>
    {
    };
    
    }  // ClientCode
    
    int main()
    {
        ClientCode::derived<int, Database::SomeDataBaseType> d;
        store(d);  // intended ClientCode::store
    }
    

    在这个例子中,main 的作者甚至不知道 Database::store 的存在。他打算调用 ClientCode::store,然后变得懒惰,让 ADL 选择函数而不是指定 ClientCode::store。毕竟,他对store 的论点与store 来自同一个命名空间,所以它应该可以工作。

    它不起作用。此示例调用Database::store。根据Database::store 的内部情况,此调用可能会导致编译时错误,或者更糟糕的是,会导致运行时错误。

    如何修复

    您对函数的命名越笼统,发生这种情况的可能性就越大。给你的内部函数(必须出现在你的标题中的那些)真正的非通用名称。或者将它们放在像details 这样的子命名空间中。在后一种情况下,您必须确保您的客户端永远不会将 details 作为用于 ADL 的关联命名空间。这通常通过不在namespace details 中创建客户端将直接或间接使用的类型来实现。

    如果您想变得更加偏执,请使用enable_if 开始锁定。

    如果您认为您的内部功能可能对您的客户有用,那么它们不再是内部的。

    上面的示例代码并不牵强。它发生在我身上。它发生在namespace std 中的函数上。我在这个例子中调用store过于通用std::advancestd::distance 是过度通用代码的经典示例。这是需要防范的。这是一个试图解决的问题。

    【讨论】:

    • 谢谢,这与我试图保护的情况非常相似,尽管我不确定我是否在浪费时间。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-21
    相关资源
    最近更新 更多