用强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>
。 也就是说,你必须在编译时certificate该channel
在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用户
可维护性
-
如果你确信通道types在可预见的未来将是一个整数,那么我会去抽象(考虑使用枚举)
-
如果你有很多使用有限值的用例,可以考虑使用模板(Jerry)
- 如果你认为,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必须被重新实现/翻译/移植/等,它更可能直接映射到其他语言。
这是抽象我的朋友! 对物体进行处理总是比较干净的