Как далеко пойти с языком со строгим контролем типов?

Скажем, я пишу 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
}

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

42
задан DanDan 5 July 2010 в 19:32
поделиться

13 ответов

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

Так что сделайте что-нибудь вроде:

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

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

60
ответ дан 26 November 2019 в 23:20
поделиться

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

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

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

Простота / легкость использования и т. Д.

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

Ремонтопригодность

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

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

  3. Если вы думаете, Канал может потенциально иметь методы сделать это класс прямо сейчас.

Усилия по кодированию Это одноразовая вещь. Так что всегда думайте о техническом обслуживании.

1
ответ дан 26 November 2019 в 23:20
поделиться

На мой взгляд, я не думаю, что то, что вы предлагаете, является большим накладным расходом, но для меня лучше сэкономить на наборе текста и просто написать в документации, что все, что за пределами 0...15, является неопределенным, и использовать assert() в функции для отлова ошибок для отладочных сборок. Я не думаю, что дополнительная сложность обеспечивает большую защиту для программистов, которые уже привыкли к программированию на языке C++, который содержит много неопределенного поведения в своих спецификациях.

1
ответ дан 26 November 2019 в 23:20
поделиться

Пример канала - сложный one:

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

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

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

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

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

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

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


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

1
ответ дан 26 November 2019 в 23:20
поделиться

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

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

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

// Currently used channel type
typedef unsigned char Channel;
6
ответ дан 26 November 2019 в 23:20
поделиться

Это абстракция дружище! Работать с объектами всегда аккуратнее

0
ответ дан 26 November 2019 в 23:20
поделиться

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

11
ответ дан 26 November 2019 в 23:20
поделиться

Если вы добавите константы для 16 различных каналов, а также статический метод, который выбирает канал для заданного значения (или генерирует исключение, если выходит за пределы диапазона), тогда это может работать без каких-либо дополнительных накладные расходы на создание объекта на вызов метода.

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

Если это API, который будет использоваться многими, то, возможно, открытие его для некоторого обзора может дать вам ценный отзыв, поскольку они, по-видимому, довольно хорошо знают предметную область API.

1
ответ дан 26 November 2019 в 23:20
поделиться

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

14
ответ дан 26 November 2019 в 23:20
поделиться

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

0
ответ дан 26 November 2019 в 23:20
поделиться

Да, идея того стоит, но (ИМО) писать полный отдельный класс для каждого диапазона целых чисел бессмысленно. Я столкнулся с достаточным количеством ситуаций, требующих целых чисел с ограниченным диапазоном, для чего я написал шаблон:

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% ситуаций.

27
ответ дан 26 November 2019 в 23:20
поделиться

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

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

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

4
ответ дан 26 November 2019 в 23:20
поделиться

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

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

Чтобы выразить эту концепцию в синтаксисе псевдо-С++ (при условии, что в C++ есть зависимые типы), вы можете написать следующее:

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

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

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

6
ответ дан 26 November 2019 в 23:20
поделиться
Другие вопросы по тегам:

Похожие вопросы: