泛型/模板编程最佳做法:限制类型,或不限制类型

Generic/template programming best practices: To limit types, or not to limit types

本文关键字:类型 编程 最佳 泛型      更新时间:2023-10-16

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

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

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

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

编辑:到目前为止,一个答案提到我正在做的库类型将很重要。 它是一个数据库库,最终可以使用STL容器,可变参数(元组),Boost Fusion等性质的东西。 我可以看到这将如何相关,但我也会对确定走哪条路的经验法则感兴趣。

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

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

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

此外,字符串和数值类型之间还有一个继承分支。

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

STL最强大的卖点之一是它是如此开放,它的算法与我的数据结构以及它提供给自己的数据结构一起工作,我的算法与它的数据结构和我的数据结构一起工作。

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

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

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

与概念非常相似,这样做的大部分动机是在未满足这些要求时简单地提供良好、有用的错误消息。最终,如果有人尝试在不符合其要求的类型上实例化模板,编译器生成错误消息。问题是,除非您采取措施确保它是,否则错误消息不会很有帮助。

问题

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

例如:

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 是过于通用的代码的典型示例。 这是需要防范的。 这是一个概念试图解决的问题。