用强types语言走多远?

比方说,我正在编写一个API,而我的一个函数带有一个代表一个通道的参数,并且只会在值0和15之间。我可以这样写:

void Func(unsigned char channel) { if(channel < 0 || channel > 15) { // throw some exception } // do something } 

还是利用C ++作为强types语言,并使自己成为一种types:

 class CChannel { public: CChannel(unsigned char value) : m_Value(value) { if(channel < 0 || channel > 15) { // throw some exception } } operator unsigned char() { return m_Value; } private: unsigned char m_Value; } 

我的function现在变成这样:

 void Func(const CChannel &channel) { // No input checking required // do something } 

但这是完全矫枉过正? 我喜欢自我logging和保证它是这样说的,但是值得付出这样一个对象的构build和破坏,更不用说所有额外的input了吗? 请让我知道你的意见和select。

如果你想这个更简单的方法来推广它,所以你可以得到更多的使用,而不是定制它到一个特定的事情。 那么问题不是“我应该为这个具体事情做一个全新的class级吗?” 但是“我应该使用我的公用设施吗? 后者总是肯定的。 公用事业总是有帮助的。

所以做一些像:

 template <typename T> void check_range(const T& pX, const T& pMin, const T& pMax) { if (pX < pMin || pX > pMax) throw std::out_of_range("check_range failed"); // or something else } 

现在你已经有了这个很好的工具来检查范围。 即使没有通道types,您的代码也可以通过使用它变得更清洁。 你可以走得更远:

 template <typename T, T Min, T Max> class ranged_value { public: typedef T value_type; static const value_type minimum = Min; static const value_type maximum = Max; ranged_value(const value_type& pValue = value_type()) : mValue(pValue) { check_range(mValue, minimum, maximum); } const value_type& value(void) const { return mValue; } // arguably dangerous operator const value_type&(void) const { return mValue; } private: value_type mValue; }; 

现在你已经有了一个很好的工具,可以这样做:

 typedef ranged_value<unsigned char, 0, 15> channel; void foo(const channel& pChannel); 

而且在其他情况下可以重复使用。 只需将其全部粘贴到"checked_ranges.hpp"文件中,并在需要时使用它。 抽象化并不坏,而且周围的公用事业并不是有害的。

另外,不要担心开销。 创build一个类只需要运行相同的代码就可以了。 另外,干净的代码比其他东西更受欢迎。 performance是最后一个问题。 一旦你完成了,那么你可以得到一个分析器来衡量(而不是猜测)缓慢的部分。

是的,这个想法是值得的,但(IMO)为每个整数范围写一个完整的,单独的类是没有意义的。 我遇到了足够的情况,要求有限的范围整数,我已经为此写了一个模板:

 template <class T, T lower, T upper> class bounded { T val; void assure_range(T v) { if ( v < lower || upper <= v) throw std::range_error("Value out of range"); } public: bounded &operator=(T v) { assure_range(v); val = v; return *this; } bounded(T const &v=T()) { assure_range(v); val = v; } operator T() { return val; } }; 

使用它会是这样的:

 bounded<unsigned, 0, 16> channel; 

当然,你可以比这更精致,但这个简单的仍然可以处理大约90%的情况。

不,这不是过度的 – 你应该总是试图将抽象表示为类。 有这么多的理由,这样做的开销是最小的。 我会打电话给class级,但不是CChannel。

不能相信没有人提到枚举的到目前为止。 将不会给你一个防弹保护,但仍然比普通的整数数据types更好。

看起来像矫枉过正,尤其是operator unsigned char()访问器。 你没有封装数据,你明显的事情更复杂,可能更容易出错。

Channel这样的数据types通常是更抽象的东西的一部分。

因此,如果您在ChannelSwitcher类中使用该types,则可以在ChannelSwitcher的正文中使用注释的typedef (可能您的typedef将被public )。

 // Currently used channel type typedef unsigned char Channel; 

无论您在构build“CChannel”对象时还是在要求约束的方法的入口处引发exception,都没有什么区别。 无论哪种情况,你都在进行运行时断言,这意味着types系统对你没有任何好处,是吗?

如果你想知道你可以用强types语言走多远,答案是“很远,但不是用C ++”。 你需要静态强制执行某种约束的能力,例如“这个方法只能用一个0到15之间的数字来调用”,需要一些称为依赖types的东西 – 也就是依赖于值的types

为了把这个概念变成伪C ++语法(假装C ++有依赖types),你可以这样写:

 void Func(unsigned char channel, IsBetween<0, channel, 15> proof) { ... } 

请注意, IsBetween而不是types参数化。 为了在你的程序中调用这个函数,你必须向编译器提供第二个参数proof ,它的types必须是IsBetween<0, channel, 15> 。 也就是说,你必须在编译时certificatechannel在0到15之间! 这个代表命题的types的概念,其价值是对这些命题的certificate,被称为咖喱 – 霍华德通信 。

当然,certificate这样的事情可能是困难的。 根据您的问题领域,成本/收益比可以轻松提示,只是在您的代码上进行运行时检查。

无论是否过分,常常取决于许多不同的因素。 在某种情况下可能会有些过火,而另一种情况则可能不会。

如果你有许多不同的function,所有可以接受的频道,都必须进行相同的范围检查,这种情况可能不会过分。 Channel类将避免代码重复,同时也提高了函数的可读性(如同命名类Channel而不是CChannel – Neil B.是对的)。

有时候,当范围足够小时,我会为input定义一个枚举。

如果为16个不同的通道添加常量,并且为给定值提取通道的静态方法(或者在超出范围时抛出exception),那么这可以在没有任何额外开销的情况下为每个方法调用创build对象。

不知道如何使用这个代码,很难说是否过度使用或不愉快。 自己尝试一下 – 用char和types安全类的方法编写几个testing用例,看看你喜欢哪一个。 如果你在写几个testing用例后感到厌倦,那么可能是最好的避免,但是如果你发现自己喜欢这个方法,那么它可能是一个守护者。

如果这是一个将被许多人使用的API,那么或许打开一些评论可能会给你有价值的反馈,因为他们大概知道API领域。

在我看来,我不认为你提出的是一个很大的开销,但对于我来说,我更喜欢保存input,只是在文档中放置0..15之外的任何东西都是未定义的,并使用assert()在函数中捕获debugging版本的错误。 我不认为增加的复杂性为那些已经习惯了C ++语言编程的程序员提供了更多的保护,这些程序员在规范中包含了很多未定义的行为。

你必须做出select。 这里没有银弹。

性能

从性能的angular度来看,开销不会太大。 (除非你要计算CPU周期)所以这很可能不是决定性因素。

简单易用等

使API简单,易于理解/学习。 你应该知道/决定数字/枚举/类是否会更容易的API用户

可维护性

  1. 如果你确信通道types在可预见的未来将是一个整数,那么我会去抽象(考虑使用枚举)

  2. 如果你有很多使用有限值的用例,可以考虑使用模板(Jerry)

  3. 如果你认为,Channel现在可能有方法使它成为一个类。

编码努力它是一次性的事情。 所以总是觉得维护。

渠道的例子是一个艰难的例子:

  • 起初,它看起来像一个简单的有限范围整数types,就像你在Pascal和Ada中find的一样。 C ++给你没有办法说这个,但枚举是够好的。

  • 如果你仔细观察,这些devise决策可能会改变吗? 你可以开始提到“频道”的频率? 通过电话(WGBH,进来)? 通过networking?

很多取决于你的计划。 API的主要目标是什么? 什么是成本模型? 频道是否会频繁创build(我怀疑不是)?

为了得到一个稍微不同的观点,让我们看看搞砸的成本:

  • 你把rep公开为int 。 客户端编写了大量的代码,这个接口要么被尊重,要么你的库停止断言失败。 创build渠道是很便宜的。 但是如果你需要改变自己的工作方式,就会失去“向后错误兼容性”,惹恼那些sl clients客户的作者。

  • 你保持抽象。 每个人都必须使用抽象(不那么糟糕),而且每个人都可以应对API的变化。 保持向后兼容性是小菜一碟。 但是创build渠道成本更高,而且更糟糕的是API必须仔细陈述何时可以安全地销毁渠道以及由谁来负责决策和销毁。 更糟糕的情况是,创build/销毁通道会导致大的内存泄漏或其他性能故障 – 在这种情况下,您将回退到枚举。

我是一个sl </s>的程序员,如果是为了我自己的工作,如果devise决定改变了下线,我会和enum一起去吃东西。 但是,如果这个API作为客户端出现给很多其他程序员,我会使用抽象。


显然我是一个道德相对主义者。

一个整数的值只能在0到15之间,是一个无符号的4位整数(或半字节,半字节)。我想如果这个通道切换逻辑是用硬件实现的,那么通道号可以表示为4位寄存器)。 如果C ++把它作为一个types,你就可以在那里完成:

 void Func(unsigned nibble channel) { // do something } 

唉,不幸的是没有。 您可以放宽API规范来表示通道号是作为无符号字符给出的,实际通道使用模16运算来计算:

 void Func(unsigned char channel) { channel &= 0x0f; // truncate // do something } 

或者,使用一个位域:

 #include <iostream> struct Channel { // 4-bit unsigned field unsigned int n : 4; }; void Func(Channel channel) { // do something with channel.n } int main() { Channel channel = {9}; std::cout << "channel is" << channel.n << '\n'; Func (channel); } 

后者可能效率较低。

我投票支持你的第一个方法,因为它更简单,更容易理解,维护和扩展,而且如果你的API必须被重新实现/翻译/移植/等,它更可能直接映射到其他语言。

这是抽象我的朋友! 对物体进行处理总是比较干净的