Как далеко зайти со строго типизированным языком? - PullRequest
42 голосов
/ 05 июля 2010

Допустим, я пишу 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
}

Но это полное излишество? Мне нравится самодокументирование и гарантия, что это то, о чем говорится, но стоит ли платить за строительство и разрушение такого объекта, не говоря уже о дополнительной печати? Пожалуйста, дайте мне знать ваши комментарии и альтернативы.

Ответы [ 14 ]

60 голосов
/ 05 июля 2010

Если вы хотели, чтобы этот более простой подход обобщил его, чтобы вы могли извлечь из него больше пользы, а не адаптировать его к конкретной вещи. Тогда возникает вопрос: «Должен ли я создать новый класс для этой конкретной вещи?» но "я должен использовать мои утилиты?"; последнее всегда да. А утилиты всегда полезны.

Так сделайте что-то вроде:

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" и используйте его, когда вам нужно. Делать абстракции никогда не бывает плохо, а использование утилит не вредно.

Кроме того, никогда не беспокойтесь о накладных расходах. Создание класса просто состоит из запуска того же кода, который вы все равно сделали бы. Кроме того, чистый код должен быть предпочтительнее всего остального; производительность является последней проблемой. Как только вы закончите, вы можете заставить профилировщик измерить (не угадать), где медленные части.

27 голосов
/ 05 июля 2010

Да, идея стоит того, но (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% ситуаций.

14 голосов
/ 05 июля 2010

Нет, это не излишне - вы всегда должны представлять абстракции как классы.Есть множество причин для этого, и накладные расходы минимальны.Я бы назвал класс Channel, а не CChannel.

11 голосов
/ 05 июля 2010

Не могу поверить, что никто не упомянул enum. Не даст вам пуленепробиваемой защиты, но все же лучше, чем простой целочисленный тип данных.

6 голосов
/ 30 июля 2010

Независимо от того, генерируете ли вы исключение при создании объекта "CChannel" или при входе в метод, требующий ограничения, не имеет большого значения. В любом случае вы делаете утверждения во время выполнения, что означает, что система типов на самом деле не приносит вам пользы, не так ли?

Если вы хотите знать, как далеко вы можете пойти со строго типизированным языком, ответ будет «очень далеко, но не с C ++». Тип силы, который вам необходим для статического применения ограничения, например, «этот метод может вызываться только с числом от 0 до 15», требует что-то, называемое зависимых типов - типы, которые зависят от значений .

Чтобы поместить концепт в псевдо-C ++ синтаксис (притворяясь, что C ++ имеет зависимые типы), вы могли бы написать так:

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

Обратите внимание, что IsBetween параметризуется значениями , а не типами . Чтобы вызвать эту функцию в вашей программе сейчас, вы должны предоставить компилятору второй аргумент proof, который должен иметь тип IsBetween<0, channel, 15>. То есть вы должны доказать во время компиляции, что channel находится между 0 и 15! Эта идея типов, которые представляют предложения, значения которых являются доказательствами этих предложений, называется соответствием Карри-Говарда .

Конечно, доказывать такие вещи может быть сложно. В зависимости от вашей проблемной области соотношение затрат и выгод может легко перевесить в пользу просто проверки времени выполнения вашего кода.

6 голосов
/ 05 июля 2010

Похоже на излишество, особенно аксессуар operator unsigned char().Вы не инкапсулируете данные, вы делаете очевидные вещи более сложными и, вероятно, более подверженными ошибкам.

Типы данных, такие как Channel, обычно являются частью чего-то более абстрактного.

Итак, если вы используете этот тип в своем классе ChannelSwitcher, вы можете использовать закомментированный typedef прямо в теле ChannelSwitcher (и, вероятно, ваш typedef будет public).

// Currently used channel type
typedef unsigned char Channel;
4 голосов
/ 05 июля 2010

То, является ли что-то излишним или нечастым, зависит от множества различных факторов. То, что может быть излишним в одной ситуации, может не быть в другой.

Этот случай не может быть излишним, если у вас было много разных функций, которые принимали все каналы и все должны были выполнять одинаковую проверку диапазона. Класс Channel позволит избежать дублирования кода, а также улучшит удобочитаемость функций (как если бы он назвал класс Channel вместо CChannel - Нил Б. прав).

Иногда, когда диапазон достаточно мал, я вместо этого определяю перечисление для ввода.

1 голос
/ 20 июля 2015

Целое число со значениями только от 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); 
}

Последний может быть менее эффективным.

1 голос
/ 06 июля 2010

Пример канала довольно сложный:

  • Сначала он выглядит как простой целочисленный тип с ограниченным диапазоном, как вы можете найти в Паскале и Аде.C ++ не дает вам возможности сказать это, но перечисление достаточно хорошее.

  • Если вы посмотрите поближе, это может быть одним из тех дизайнерских решенийчто может измениться? Не могли бы вы начать ссылаться на "канал" по частоте?По звонкам (WGBH, входите)?По сети?

Многое зависит от ваших планов.Какова основная цель API?Сколько стоит модель?Будут ли каналы создаваться очень часто (я подозреваю, что нет)?

Чтобы получить немного иную перспективу, давайте посмотрим на стоимость порчи:

  • Вы представляете представителя как int.Клиенты пишут много кода, интерфейс либо соблюдается, либо ваша библиотека останавливается с ошибкой подтверждения.Создание каналов очень дешево.Но если вам нужно изменить способ работы, вы потеряете «обратную совместимость с ошибками» и раздражите авторов неаккуратных клиентов.

  • Вы оставляете это абстрактным.Каждый должен использовать абстракцию (не так уж плохо), и все защищены от изменений в API.Поддержание обратной совместимости - это очень просто.Но создание каналов обходится дороже, и, что еще хуже, API должен тщательно указывать, когда безопасно уничтожить канал и кто несет ответственность за решение и уничтожение.В худшем случае создание / уничтожение каналов приводит к большой утечке памяти или другому сбою производительности - в этом случае вы возвращаетесь к перечислению.

Я неряшливый программист, иесли бы это было для моей собственной работы, я бы пошел с перечислением и съел бы стоимость, если проектное решение изменилось в будущем.Но если бы этот API предназначался для многих других программистов в качестве клиентов, я бы использовал абстракцию.


Очевидно, я моральный релятивист.

1 голос
/ 06 июля 2010

Вы должны сделать выбор.Здесь нет серебряной пули.

Производительность

С точки зрения производительности, накладные расходы не будут большими, если вообще будут.(если только вы не подсчитали циклы процессора). Поэтому, скорее всего, это не должно быть определяющим фактором.

Простота / простота использования и т. д.

Создание APIпросто и легко понять / выучить.Вы должны знать / решить, будут ли числа / перечисления / класс проще для пользователя API

Поддерживаемость

  1. Если вы уверены, что каналтип будет целым числом в обозримом будущем, я бы обошелся без абстракции (рассмотрим использование перечислений)

  2. Если у вас много вариантов использования для ограниченных значений, рассмотрите возможность использованияшаблоны (Джерри)

  3. Если вы думаете, у Channel потенциально могут быть методы, делающие его классом прямо сейчас.

Кодирование Это единое целоевремя вещь.Поэтому всегда думайте о техобслуживании.

...