强类型语言还能走多远?
假设我正在编写一个 API,我的一个函数采用一个代表通道的参数,并且只能在值 0 和 15 之间。我可以这样写:
void Func(unsigned char channel)
{
if(channel < 0 || channel > 15)
{ // throw some exception }
// do something
}
或者我是否利用 C++ 的强大优势类型语言,并使自己成为一种类型:
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;
}
我的功能现在变成了这样:
void Func(const CChannel &channel)
{
// No input checking required
// do something
}
但这完全是多余的吗?我喜欢自我文档和保证它就是它所说的那样,但是值得为这样一个对象的构建和销毁付出代价,更不用说所有额外的打字了?请让我知道您的意见和替代方案。
Let's say I am writing an API, and one of my functions take a parameter that represents a channel, and will only ever be between the values 0 and 15. I could write it like this:
void Func(unsigned char channel)
{
if(channel < 0 || channel > 15)
{ // throw some exception }
// do something
}
Or do I take advantage of C++ being a strongly typed language, and make myself a type:
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;
}
My function now becomes this:
void Func(const CChannel &channel)
{
// No input checking required
// do something
}
But is this total overkill? I like the self-documentation and the guarantee it is what it says it is, but is it worth paying the construction and destruction of such an object, let alone all the additional typing? Please let me know your comments and alternatives.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(14)
如果您想要这种更简单的方法,请对其进行概括,以便您可以更多地利用它,而不是针对特定事物进行定制。那么问题不是“我应该为这个特定的事情创建一个全新的类吗?”但是“我应该使用我的公用事业吗?”;后者总是肯定的。实用程序总是有帮助的。
因此,请执行以下操作:
现在您已经有了这个用于检查范围的好实用程序。即使没有通道类型,您的代码也可以通过使用它而变得更干净。您可以更进一步:
现在您已经有了一个很好的实用程序,并且可以执行以下操作:
并且它可以在其他场景中重复使用。只需将其全部放入
"checked_ranges.hpp"
文件中,并在需要时使用它即可。进行抽象从来都不是坏事,并且拥有实用程序也没有什么坏处。另外,永远不用担心开销。创建一个类只需运行您无论如何都会执行的相同代码。此外,干净的代码比其他任何东西都更重要;性能是最后一个问题。完成后,您可以使用分析器来测量(而不是猜测)缓慢部分的位置。
If you wanted this simpler approach generalize it so you can get more use out of it, instead of tailor it to a specific thing. Then the question is not "should I make a entire new class for this specific thing?" but "should I use my utilities?"; the latter is always yes. And utilities are always helpful.
So make something like:
Now you've already got this nice utility for checking ranges. Your code, even without the channel type, can already be made cleaner by using it. You can go further:
Now you've got a nice utility, and can just do:
And it's re-usable in other scenarios. Just stick it all in a
"checked_ranges.hpp"
file and use it whenever you need. It's never bad to make abstractions, and having utilities around isn't harmful.Also, never worry about overhead. Creating a class simply consists of running the same code you would do anyway. Additionally, clean code is to be preferred over anything else; performance is a last concern. Once you're done, then you can get a profiler to measure (not guess) where the slow parts are.
是的,这个想法是值得的,但是(IMO)为每个整数范围编写一个完整的、单独的类是毫无意义的。我遇到过足够多的需要有限范围整数的情况,因此我为此目的编写了一个模板:
使用它会类似于:
当然,您可以比这更复杂,但是这个简单的仍然可以处理大约 90 %的情况很好。
Yes, the idea is worthwhile, but (IMO) writing a complete, separate class for each range of integers is kind of pointless. I've run into enough situations that call for limited range integers that I've written a template for the purpose:
Using it would be something like:
Of course, you can get more elaborate than this, but this simple one still handles about 90% of situations pretty well.
不,这并不是矫枉过正——您应该始终尝试将抽象表示为类。这样做有无数的理由,而且开销很小。不过,我会将该类称为 Channel,而不是 CChannel。
No, it is not overkill - you should always try to represent abstractions as classes. There are a zillion reasons for doing this and the overhead is minimal. I would call the class Channel though, not CChannel.
不敢相信到目前为止还没有人提到枚举。不会为您提供万无一失的保护,但仍然比普通整数数据类型更好。
Can't believe nobody mentioned enum's so far. Won't give you a bulletproof protection, but still better than a plain integer datatype.
看起来有点矫枉过正,尤其是
operator unsigned char()
访问器。您没有封装数据,而是使明显的事情变得更加复杂,并且可能更容易出错。像
Channel
这样的数据类型通常是更抽象的事物的一部分。因此,如果您在
ChannelSwitcher
类中使用该类型,则可以在ChannelSwitcher
的主体中使用带注释的 typedef (并且您的 typedef 可能会成为公开
)。Looks like overkill, especially the
operator unsigned char()
accessor. You're not encapsulating data, you're making evident things more complicated and, probably, more error-prone.Data types like your
Channel
are usually a part of something more abstracted.So, if you use that type in your
ChannelSwitcher
class, you could use commented typedef right in theChannelSwitcher
's body (and, probably, your typedef is going to bepublic
).无论是在构造“CChannel”对象时抛出异常,还是在需要约束的方法入口处抛出异常,都没有什么区别。无论哪种情况,您都会进行运行时断言,这意味着类型系统实际上对您没有任何好处,不是吗?
如果您想知道使用强类型语言可以走多远,答案是“非常远,但 C++ 不行”。静态强制执行约束(例如“只能使用 0 到 15 之间的数字来调用此方法”)所需的功能需要称为 依赖类型——即依赖于值的类型。
要将这个概念放入伪 C++ 语法中(假装 C++ 具有依赖类型),您可以这样写:
请注意,
IsBetween
是通过值而不是类型<来参数化的。 /em>。现在,为了在程序中调用此函数,您必须向编译器提供第二个参数proof
,该参数的类型必须为IsBetween<0, channel, 15>
。也就是说,您必须在编译时证明channel
在0到15之间!这种代表命题的类型的想法,其值是这些命题的证明,被称为 库里-霍华德通讯。当然,证明这些事情可能很困难。根据您的问题领域,成本/效益比很容易倾向于仅对代码进行运行时检查。
Whether you throw an exception when constructing your "CChannel" object or at the entrance to the method that requires the constraint makes little difference. In either case you're making runtime assertions, which means the type system really isn't doing you any good, is it?
If you want to know how far you can go with a strongly typed language, the answer is "very far, but not with C++." The kind of power you need to statically enforce a constraint like, "this method may only be invoked with a number between 0 and 15" requires something called dependent types--that is, types which depend on values.
To put the concept into pseudo-C++ syntax (pretending C++ had dependent types), you might write this:
Note that
IsBetween
is parameterized by values rather than types. In order to call this function in your program now, you must provide to the compiler the second argument,proof
, which must have the typeIsBetween<0, channel, 15>
. Which is to say, you have to prove at compile-time thatchannel
is between 0 and 15! This idea of types which represent propositions, whose values are proofs of those propositions, is called the Curry-Howard Correspondence.Of course, proving such things can be difficult. Depending on your problem domain, the cost/benefit ratio can easily tip in favor of just slapping runtime checks on your code.
某件事是否过度杀伤通常取决于许多不同的因素。在一种情况下可能太过分的事情在另一种情况下可能不会。
如果您有许多不同的功能,所有接受的通道都必须执行相同的范围检查,那么这种情况可能不会太过分。 Channel 类可以避免代码重复,并且还可以提高函数的可读性(就像将类命名为 Channel 而不是 CChannel - Neil B. 是对的)。
有时,当范围足够小时,我会为输入定义一个枚举。
Whether something is overkill or not often depends on lots of different factors. What might be overkill in one situation might not in another.
This case might not be overkill if you had lots of different functions that all accepted channels and all had to do the same range checking. The Channel class would avoid code duplication, and also improve readability of the functions (as would naming the class Channel instead of CChannel - Neil B. is right).
Sometimes when the range is small enough I will instead define an enum for the input.
如果您为 16 个不同的通道添加常量,并且还添加一个静态方法来获取给定值的通道(或者如果超出范围则抛出异常),那么这可以工作,而无需为每个方法调用创建任何额外的对象开销。
在不知道如何使用这段代码的情况下,很难说它是否过度杀伤或使用起来是否愉快。自己尝试一下 - 使用 char 和类型安全类这两种方法编写一些测试用例 - 然后看看您喜欢哪种。如果您在编写了一些测试用例后厌倦了它,那么最好避免它,但如果您发现自己喜欢这种方法,那么它可能是一个保留者。
如果这是一个将被许多人使用的 API,那么也许将其开放给一些审查可能会给您提供有价值的反馈,因为他们可能非常了解 API 领域。
If you add constants for the 16 different channels, and also a static method that fetches the channel for a given value (or throws an exception if out of range) then this can work without any additional overhead of object creation per method call.
Without knowing how this code is going to be used, it's hard to say if it's overkill or not or pleasant to use. Try it out yourself - write a few test cases using both approaches of a char and a typesafe class - and see which you like. If you get sick of it after writing a few test cases, then it's probably best avoided, but if you find yourself liking the approach, then it might be a keeper.
If this is an API that's going to be used by many, then perhaps opening it up to some review might give you valuable feedback, since they presumably know the API domain quite well.
在我看来,我不认为你提出的建议是一个很大的开销,但对我来说,我更喜欢保存输入,只在文档中放入 0..15 之外的任何内容都是未定义的,并使用assert()在函数中捕获调试版本的错误。我认为增加的复杂性并没有为已经习惯 C++ 语言编程的程序员提供更多的保护,因为 C++ 语言的规范中包含很多未定义的行为。
In my opinion, I don't think what you are proposing is a big overhead, but for me, I prefer to save the typing and just put in the documentation that anything outside of 0..15 is undefined and use an assert() in the function to catch errors for debug builds. I don't think the added complexity offers much more protection for programmers who are already used to C++ language programming which contains alot of undefined behaviours in its specs.
你必须做出选择。这里没有灵丹妙药。
性能
从性能角度来看,开销即使有也不会太多。 (除非你必须计算CPU周期)所以很可能这不应该是决定因素。
简单/易于使用等
使 API 简单且易于理解/学习。
您应该知道/决定数字/枚举/类对于 api 用户来说是否更容易
可维护性
如果您非常确定渠道
类型将是一个整数
在可预见的未来,我会去
没有抽象(考虑
使用枚举)
如果您有很多用例
有界值,考虑使用
模板 (Jerry)
可能有方法使它成为
现在上课。
编码工作
这是一次性的事情。所以要时刻思考维护。
You have to make a choice. There is no silver bullet here.
Performance
From the performance perspective, the overhead isn't going to be much if at all. (unless you've got to counting cpu cycles) So most likely this shouldn't be the determining factor.
Simplicity/ease of use etc
Make the API simple and easy to understand/learn.
You should know/decide whether numbers/enums/class would be easier for the api user
Maintainability
If you are very sure the channel
type is going to be an integer in
the foreseeable future , I would go
without the abstraction (consider
using enums)
If you have a lot of use cases for a
bounded values, consider using the
templates (Jerry)
potentially have methods make it a
class right now.
Coding effort
Its a one time thing. So always think maintenance.
通道示例是一个困难的示例:
乍一看,它看起来像一个简单的有限范围整数类型,就像您在 Pascal 和 Ada 中看到的那样。 C++ 无法让您这么说,但是枚举就足够了。
如果你仔细观察,它可能是那些可能会改变的设计决策之一吗? strong> 您能开始按频率提及“频道”吗?通过呼唤信(WGBH,进来)?通过网络?
很大程度上取决于您的计划。 API 的主要目标是什么?成本模型是什么?频道会被频繁创建吗(我怀疑不会)?
为了获得稍微不同的视角,让我们看看搞砸的成本:
您将代表公开为
int
。客户端编写大量代码,接口要么受到尊重,要么您的库因断言失败而停止。创建渠道非常便宜。但如果您需要改变做事的方式,您就会失去“向后错误兼容性”并惹恼草率客户端的作者。你让它保持抽象。每个人都必须使用抽象(还不错),并且每个人都可以应对 API 的未来变化。保持向后兼容性是小菜一碟。但创建通道的成本更高,更糟糕的是,API 必须仔细说明何时可以安全地销毁通道以及谁负责决策和销毁。最糟糕的情况是,创建/销毁通道会导致严重的内存泄漏或其他性能故障,在这种情况下,您将退回到枚举。
我是一个草率的程序员,如果是为了我自己的工作,我会选择枚举,并在设计决策发生变化时承担成本。但如果这个 API 作为客户提供给许多其他程序员,我会使用抽象。
显然我是一个道德相对主义者。
The channel example is a tough one:
At first it looks like a simple limited-range integer type, like you find in Pascal and Ada. C++ gives you no way to say this, but an enum is good enough.
If you look closer, could it be one of those design decisions that are likely to change? Could you start referring to "channel" by frequency? By call letters (WGBH, come in)? By network?
A lot depends on your plans. What's the main goal of the API? What's the cost model? Will channels be created very frequently (I suspect not)?
To get a slightly different perspective, let's look at the cost of screwing up:
You expose the rep as
int
. Clients write a lot of code, the interface is either respected or your library halts with an assertion failure. Creating channels is dirt cheap. But if you need to change the way you're doing things, you lose "backward bug-compatibility" and annoy authors of sloppy clients.You keep it abstract. Everybody has to use the abstraction (not so bad), and everybody is futureproofed against changes in the API. Maintaining backwards compatibility is a piece of cake. But creating channels is more costly, and worse, the API has to state carefully when it is safe to destroy a channel and who is responsible for the decision and the destruction. Worse case scenario is that creating/destroying channels leads to a big memory leak or other performance failure—in which case you fall back to the enum.
I'm a sloppy programmer, and if it were for my own work, I'd go with the enum and eat the cost if the design decision changes down the line. But if this API were to go out to a lot of other programmers as clients, I'd use the abstraction.
Evidently I'm a moral relativist.
值仅在 0 到 15 之间的整数是无符号 4 位整数(或半字节、半字节)。我想如果此通道切换逻辑在硬件中实现,那么通道号可能表示为 4位寄存器)。
如果 C++ 具有这种类型,那么您就可以完成此操作:
唉,不幸的是它没有。您可以放宽 API 规范,将通道号表示为无符号字符,并使用模 16 运算计算实际通道:
或者使用位字段:
后者可能效率较低。
An integer with values only ever between 0 and 15 is an unsigned 4-bit integer (or half-byte, nibble. I imagine if this channel switching logic would be implemented in hardware, then the channel number might be represented as that, a 4-bit register).
If C++ had that as a type you would be done right there:
Alas, unfortunately it doesn't. You could relax the API specification to express that the channel number is given as an unsigned char, with the actual channel being computed using a modulo 16 operation:
Or, use a bitfield:
The latter might be less efficient.
我投票支持你的第一种方法,因为它更简单、更容易理解、维护和扩展,并且因为如果你的 API 必须重新实现/翻译/移植等,它更有可能直接映射到其他语言。
I vote for your first approach, because it's simpler and easier to understand, maintain, and extend, and because it is more likely to map directly to other languages should your API have to be reimplemented/translated/ported/etc.
这是抽象我的朋友!使用对象总是更整洁
This is abstraction my friend! It's always neater to work with objects