Подтвердить что ты не робот

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

Скажем, я пишу API, и одна из моих функций принимает параметр, который представляет канал, и будет только когда-либо между значениями 0 и 15. Я мог бы написать это следующим образом:

void Func(unsigned char channel)
{
    if(channel < 0 || channel > 15)
    { // throw some exception }
    // do something
}

Или я использую С++, являющийся строго типизированным языком, и сделаю себе тип:

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
}

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

4b9b3361

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

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

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

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

// Currently used channel type
typedef unsigned char Channel;

Ответ 6

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

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

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

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

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

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

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

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

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

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

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

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

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

Усиление кодирования Это одно время. Поэтому всегда думайте о техническом обслуживании.

Ответ 11

Пример канала является жестким:

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

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

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

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

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

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

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


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

Ответ 12

Целое число со значениями, имеющими значения только от 0 до 15, представляет собой неподписанное 4-разрядное целое число (или полубайт, полубайт). Я предполагаю, что если эта логика переключения каналов будет реализована на аппаратном уровне, тогда номер канала может быть представлен как, 4-битный регистр). Если бы С++ имел это как тип, вы бы сделали это прямо здесь:

void Func(unsigned nibble channel)
{
    // do something
}

Увы, к сожалению, этого не происходит. Вы можете уменьшить спецификацию API, чтобы выразить, что номер канала указан как unsigned char, при этом фактический канал вычисляется с использованием операции по модулю 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

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

Ответ 14

Это абстракция, мой друг! Он всегда работает с объектами