42

假设我正在编写一个 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
}

但这完全是矫枉过正吗?我喜欢自我记录和保证它是它所说的那样,但是值得为这样一个对象的构造和销毁付出代价,更不用说所有额外的打字了吗?请让我知道您的意见和替代方案。

4

14 回答 14

60

如果您想要这种更简单的方法,请对其进行概括,以便您可以从中获得更多用途,而不是针对特定事物进行定制。那么问题不是“我应该为这个特定的东西制作一个全新的课程吗?” 但是“我应该使用我的公用事业吗?”;后者总是是的。实用程序总是有帮助的。

所以做这样的事情:

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
}

现在你已经有了这个检查范围的好工具。您的代码,即使没有通道类型,也可以通过使用它变得更简洁。你可以更进一步:

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"文件中,并在需要时使用它。进行抽象从来都不是坏事,并且拥有实用程序也无害。

此外,永远不要担心开销。创建一个类只需运行您无论如何都会执行的相同代码。此外,干净的代码比其他任何东西都更受欢迎;性能是最后一个问题。完成后,您可以使用分析器来测量(而不是猜测)慢速部分的位置。

于 2010-07-05T19:44:26.473 回答
27

是的,这个想法是值得的,但是(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% 的情况。

于 2010-07-05T19:49:47.913 回答
14

不,这并不过分——您应该始终尝试将抽象表示为类。这样做的原因有很多,而且开销很小。不过,我会调用类 Channel,而不是 CChannel。

于 2010-07-05T19:37:26.753 回答
11

不敢相信到目前为止没有人提到枚举。不会为您提供防弹保护,但仍比普通整数数据类型好。

于 2010-07-05T19:51:02.753 回答
6

看起来有点矫枉过正,尤其是operator unsigned char()存取器。您不是在封装数据,而是使明显的事情变得更加复杂,并且可能更容易出错。

像您这样的数据类型Channel通常是更抽象的东西的一部分。

因此,如果您在类中使用该类型,则可以在' 的正文ChannelSwitcher中使用带注释的 typedef (并且,您的 typedef 可能会是)。ChannelSwitcherpublic

// Currently used channel type
typedef unsigned char Channel;
于 2010-07-05T19:34:28.233 回答
6

无论是在构造“CChannel”对象时还是在需要约束的方法的入口处抛出异常都没有什么区别。无论哪种情况,您都在进行运行时断言,这意味着类型系统确实对您没有任何好处,是吗?

如果您想知道使用强类型语言可以走多远,答案是“非常远,但使用 C++ 不行”。您需要静态强制执行约束(例如“此方法只能使用 0 到 15 之间的数字调用”)的那种能力需要称为依赖类型的东西——即依赖于值的类型

要将概念放入伪 C++ 语法(假设 C++ 具有依赖类型),您可以这样写:

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

请注意,它IsBetween是由values而不是types参数化的。为了现在在您的程序中调用此函数,您必须向编译器提供第二个参数 ,proof它的类型必须为IsBetween<0, channel, 15>。也就是说,你必须在编译时证明channel在 0 到 15 之间!这种表示命题的类型的概念,其值是这些命题的证明,被称为Curry-Howard Correspondence

当然,证明这样的事情可能很困难。根据您的问题领域,成本/收益比很容易倾向于只对您的代码进行运行时检查。

于 2010-07-30T10:43:57.470 回答
4

某事是否过度杀伤通常取决于许多不同的因素。在一种情况下可能过度杀伤力的东西在另一种情况下可能不会。

如果您有许多不同的功能,所有接受的通道都必须进行相同的范围检查,那么这种情况可能不会过大。Channel 类将避免代码重复,并提高函数的可读性(就像将类命名为 Channel 而不是 CChannel - Neil B. 是正确的)。

有时当范围足够小时,我会改为为输入定义一个枚举。

于 2010-07-05T19:45:41.073 回答
1

如果您为 16 个不同的通道添加常量,以及为给定值获取通道的静态方法(如果超出范围则抛出异常),那么这可以在每个方法调用没有任何额外的对象创建开销的情况下工作。

在不知道将如何使用此代码的情况下,很难说它是否矫枉过正或使用起来是否愉快。自己尝试一下 - 使用 char 和类型安全类的两种方法编写一些测试用例 - 看看你喜欢哪个。如果您在编写了几个测试用例后厌倦了它,那么最好避免它,但如果您发现自己喜欢这种方法,那么它可能是一个守门员。

如果这是一个将被许多人使用的 API,那么也许打开它进行一些审查可能会给您提供有价值的反馈,因为他们可能非常了解 API 领域。

于 2010-07-05T19:45:02.917 回答
1

在我看来,我不认为你提出的开销很大,但对我来说,我更喜欢保存打字,只是在文档中放入 0..15 之外的任何内容未定义并使用 assert()在函数中捕获调试构建的错误。我不认为增加的复杂性为已经习惯于 C++ 语言编程的程序员提供了更多的保护,C++ 语言在其规范中包含许多未定义的行为。

于 2010-07-05T20:31:04.367 回答
1

你必须做出选择。这里没有灵丹妙药。

表现

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

简单/易用等

使 API 简单易懂/易学。您应该知道/决定数字/枚举/类对于 api 用户是否更容易

可维护性

  1. 如果您非常确定在可预​​见的将来通道类型将是整数,我将不使用抽象(考虑使用枚举)

  2. 如果您有很多有界值的用例,请考虑使用模板 (Jerry)

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

编码工作 它是一次性的。所以总是想着保养。

于 2010-07-05T20:38:49.387 回答
1

频道示例是一个艰难的示例:

  • 起初它看起来像一个简单的有限范围整数类型,就像你在 Pascal 和 Ada 中看到的那样。C++ 无法让您这么说,但枚举就足够了。

  • 如果您仔细观察,它会是那些可能会改变的设计决策之一吗? 你能开始按频率指代“频道”吗?通过电话信件(WGBH,进来)?通过网络?

很大程度上取决于你的计划。API 的主要目标是什么?什么是成本模型?是否会非常频繁地创建频道(我怀疑不会)?

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

  • 您将代表公开为int. 客户编写了大量代码,要么尊重接口,要么你的库因断言失败而停止。创建渠道非常便宜。但是如果你需要改变你做事的方式,你就会失去“向后的错误兼容性”,并且会惹恼那些草率客户的作者。

  • 你保持抽象。每个人都必须使用抽象(还不错),并且每个人都可以防止 API 发生变化。保持向后兼容性是小菜一碟。但是创建通道的成本更高,更糟糕的是,API 必须仔细说明何时可以安全地销毁通道以及谁负责决定和销毁。更糟糕的情况是创建/销毁通道会导致大量内存泄漏或其他性能故障——在这种情况下,你会退回到枚举。

我是一个草率的程序员,如果是为了我自己的工作,如果设计决定发生变化,我会选择枚举并吃掉成本。但是,如果这个 API 要作为客户提供给许多其他程序员,我会使用抽象。


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

于 2010-07-06T03:19:55.867 回答
1

值仅在 0 到 15 之间的整数是无符号的 4 位整数(或半字节,半字节。我想如果此通道切换逻辑将在硬件中实现,那么通道号可能表示为 4位寄存器)。如果 C++ 将其作为一种类型,那么您将在那里完成:

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); 
}

后者可能效率较低。

于 2015-07-20T16:31:08.867 回答
0

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

于 2010-07-05T20:48:38.327 回答
0

这是抽象我的朋友!处理对象总是更整洁

于 2010-07-05T20:55:25.413 回答