【问题标题】:How far to go with a strongly typed language?强类型语言能走多远?
【发布时间】:2010-07-05 19:32:04
【问题描述】:

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

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

【问题讨论】:

  • FWIW,由于参数类型是unsigned,所以没有测试channel &lt; 0的目的。
  • 随机有趣的事实:Ada 本身就支持这一点,如 type CChannel is range 0..15;,几年前有一个 paper 关于向 C 添加类似的功能
  • @Michael: ...Ada 从 Pascal 那里得到它,它只是稍微改变了语法 (type CChannel = 0 .. 15;)。
  • 既然您已经回答了您提出的问题,您是否考虑过是否真的应该拥有一个class Channel 和一个成员Channel::Func()
  • 为什么要让Func 成为会员?这既不是 Java 也不是 C#!

标签: c++ api types strong-typing


【解决方案1】:

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

所以做这样的事情:

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

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

【讨论】:

  • +1。考虑到这与我发布的内容有多么相似,如果我 没有 投赞成票,那几乎是虚伪的! :-)
  • +1,但是,你可以使用某种明确的 typedef(我知道 boost 有一个),所以如果你碰巧将它与其他一些 ranged_value 。当然,在这种情况下,可论证的运算符 value_type 重载被删​​除了。
  • 如果您有另一个typedef ranged_value&lt;unsigned char, 0, 15&gt; potatoe;,则存在传递potatoe 实例而不是channel 实例的风险。为此,Boost 有Strong Typedef。否则,总是有解决方案使用第 4 个判别模板参数从匿名命名空间传入标签。
  • @GMan:哈哈,喜欢这句话。在这种情况下,它很容易解决:)
  • @MartinDrozdik:只是更容易从类外部获取模板参数。
【解决方案2】:

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

【讨论】:

  • +1,我将此问题添加到我的收藏夹以获得此答案!
  • +1。考虑到这与我发布的内容有多么相似,如果我 没有 给它投票,那几乎是虚伪的! :-) :P
  • 现在我再看一遍,怎么用?我不能做channel c; c = 5;,因为它没有默认构造函数,我也不能做channel c(5);,因为它没有转换构造函数。
  • 尽管现在您可以摆脱复制赋值运算符,因为默认使用构造函数来创建要从中赋值的对象。 (然后我们的 cmets 将真正匹配。:P)
  • 你可以,但我不喜欢——赋值运算符是一个相当重要的优化(它避免了在分配我们知道在范围内的值时进行范围检查)。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • +1 - 正要写完全相同的东西(几乎是逐字逐句)。
    • 说到对象,从 0 到 15 的范围与 16 个定义的值不同。
    • 好的,现在我想要一个代表大于 11 的数字的类型。给我看代表它的枚举。
    • 枚举 SomeNaturalNumbers {One=1, Twelve=12};
    【解决方案5】:

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

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

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

    // Currently used channel type
    typedef unsigned char Channel;
    

    【讨论】:

    • 我认为typedef 是处理这个问题的好方法,除非Channel 有自己的频道特定行为(例如openconnectsend。 ..),或者如果 Channel. 有可能在未来需要子类
    • @Frustrated 我实际上认为第二种情况使类的使用非常明显:)
    • @FrustratedWithFormsDesigner - Channel 类可能只保留这个值。 @Kotti - 我仍然需要限制检查 typedef,但是是的,这似乎比我的第一个解决方案更可取。
    【解决方案6】:

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

    如果您想知道使用强类型语言可以走多远,答案是“非常远,但使用 C++ 不行”。静态强制执行诸如“此方法只能使用 0 到 15 之间的数字”之类的约束所需的能力需要称为 dependent types 的东西——也就是说,types这取决于值

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

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

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

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

    【讨论】:

    • 谢谢,这是一个很好的评论。你认为 C++ 会看到这样的特性吗?
    • 不,我认为这不太可能。
    【解决方案7】:

    某事是否矫枉过正通常取决于许多不同的因素。在一种情况下可能过分的事情在另一种情况下可能不会。

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

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

    【讨论】:

      【解决方案8】:

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

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

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

      【讨论】:

        【解决方案9】:

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

        【讨论】:

          【解决方案10】:

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

          性能

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

          简单/易用等

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

          可维护性

          1. 如果您非常确定频道 类型将是一个整数 可预见的未来,我会去 没有抽象(考虑 使用枚举)

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

          3. 如果您认为,Channel 可以 可能有方法使它成为 马上上课。

          编码工作 它是一次性的。所以总是要考虑维护。

          【讨论】:

            【解决方案11】:

            频道示例很难:

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

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

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

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

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

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

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


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

            【讨论】:

              【解决方案12】:

              值仅在 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); 
              }
              

              后者可能效率较低。

              【讨论】:

              • 对于这种特殊情况,使用半字节可能没问题,但它会将值(通道 13)与表示(0xE)紧密联系在一起。在另一个任意示例中,如果需要进行范围检查以确保音量在 0-11 范围内怎么办?
              • @Landon 是的,这是一个非常具体的答案,仅在这种特殊情况下有效,假设总是有 15 个通道。我通常会尝试以最具体的形式解决问题,然后在需要时进行概括。
              【解决方案13】:

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

              【讨论】:

                【解决方案14】:

                这是抽象我的朋友!使用对象总是更整洁

                【讨论】:

                  猜你喜欢
                  • 2011-01-20
                  • 2023-03-11
                  • 2023-03-11
                  • 2013-02-03
                  • 1970-01-01
                  • 2011-02-11
                  • 2010-09-05
                  • 2019-12-13
                  • 2013-03-06
                  相关资源
                  最近更新 更多