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

Является ли идиоматично нормально поставить алгоритм в класс?

У меня сложный алгоритм. Это использует много переменных, вычисляет вспомогательные массивы при инициализации, а также вычисляет массивы на этом пути. Поскольку алгоритм является сложным, я разбиваю его на несколько функций.

Теперь я действительно не вижу, как это может быть класс от идиоматического пути; Я имею в виду, я просто использовал алгоритмы как функции. Это будет просто:

Calculation calc(/* several parameters */);
calc.calculate();
// get the heterogenous results via getters

С другой стороны, включение этого в класс имеет следующие преимущества:

  • Мне не нужно передавать все переменные другим функциям/методам.
  • массивы, инициализированные в начале алгоритма, доступны по всему классу в каждой функции
  • мой код короче и (imo) clearer

Гибридным способом было бы поместить класс алгоритма в исходный файл и получить доступ к нему через функцию, которая его использует. Пользователь алгоритма не увидит класс.

Есть ли у кого-нибудь ценные мысли, которые могут помочь мне?

Спасибо вам заблаговременно!

4b9b3361

Ответ 1

У меня сложный алгоритм. Это использует множество переменных, вычисляет вспомогательные массивы при инициализации, а также вычисляет массивы на этом пути. [...]

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

Это не так, но многие люди делают то же самое, что и вы (так я сделал несколько раз).

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

То есть вместо:

Calculation calc(a, b, c, d, e, f, g);
calc.calculate();
// use getters on calc from here on

вы можете написать:

CalcInputs inputs(a, b, c, d, e, f, g);
CalcResult output = calculate(inputs); // calculate is now free function
// use getters on output from here on

Это не создает никаких проблем и выполняет ту же (фактически лучшую) группировку данных.

Ответ 2

Я бы сказал, что очень идиоматично представлять алгоритм (или, может быть, лучше, вычисление) как класс. Одним из определений класса объектов из ООП является "данные и функции для работы с этими данными". Алгоритм compex с его входами, выходами и промежуточными данными идеально подходит для этого определения.

Я делал это сам несколько раз, и он значительно упрощает (человеческий) анализ потока кода, делая все проще рассуждать, отлаживать и тестировать.

Ответ 3

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

double calculation( /* input parameters */ )
{
    SupportClass calc( /* input parameters */ );
    calc.part1();
    calc.part2();
    //  etc...
    return calc.results();
}

В зависимости от того, как организован ваш код, SupportClass будет в неназванном пространстве имен в исходном файле (возможно, наиболее общий случай) или в заголовке "private", который включен только источники, участвующие в алгоритме.

Ответ 4

Это действительно сводится к: нужно ли алгоритму получить доступ к частной области класса, которая не должна быть общедоступной? Если ответ "да" (если вы не хотите реорганизовать интерфейс вашего класса, в зависимости от конкретных случаев), вы должны пойти с функцией-членом, если нет, то свободная функция достаточно хороша.

Возьмем, к примеру, стандартную библиотеку. Большинство алгоритмов предоставляются в виде бесплатных функций, поскольку они имеют доступ только к общему интерфейсу класса (например, к итераторам для стандартных контейнеров).

Ответ 5

Вам нужно каждый раз вызывать одни и те же функции в одном и том же порядке? Тогда вам не нужно требовать вызова кода для этого. Разделение вашего алгоритма на несколько функций прекрасное, но у меня все равно будет один вызов следующий, а затем следующий и т.д., С передачей структуры результатов/параметров. Класс не подходит для однократного вызова какой-либо процедуры.

Единственный способ сделать это с классом состоит в том, что класс инкапсулирует все входные данные, а затем вызывается myClass.nameOfMyAlgorithm() на нем, среди других потенциальных операций. Затем у вас есть данные + манипуляторы. Но просто манипуляторы? Да, я не уверен.

Ответ 7

В современном С++ различие было несколько подорвано. Даже из-за перегрузки оператором языка pre-ANSI вы можете создать класс, экземпляры которого являются синтаксически подобными функциями:

struct Multiplier
{
    int factor_;

    Multiplier(int f) : factor_(f) { }

    int operator()(int v) const
    {
        return v * _factor;
    }
};

Multipler doubler(2);
std::cout << doubler(3) << std::endl; // prints 6

Такой класс /struct называется функтором и может захватывать "контекстуальные" значения в его конструкторе. Это позволяет эффективно передавать параметры функции в два этапа: некоторые в вызове конструктора, некоторые позже, каждый раз, когда вы вызываете это по-настоящему. Это называется приложением частичной функции.

Чтобы связать это с вашим примером, ваша функция-член calculate может быть преобразована в operator(), а затем экземпляр Calculation будет функцией! (или почти достаточно.)

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

Вместо того, чтобы спрашивать: "Должен ли я помещать этот алгоритм в функцию или класс?" вместо этого спросите себя: "Было бы полезно иметь возможность передавать параметры этому алгоритму на двух или более этапах?" В вашем примере все параметры входят в конструктор, а в последнем вызове calculate нет ни одного, поэтому нет смысла запрашивать у пользователей вашего класса два вызова.

В С++ 11 различие расходится еще дальше (и все становится намного удобнее), в знак признания текучести этих идей:

auto doubler = [] (int val) { return val * 2; };

std::cout << doubler(3) << std::endl; // prints 6

Здесь doubler представляет собой лямбда, что по сути является отличным способом объявить экземпляр класса, генерируемого компилятором, который реализует оператор ().

Повторяя исходный пример более точно, нам нужна функция-подобная вещь под названием multiplier, которая принимает factor, и возвращает другую функцию-подобную вещь, которая принимает значение v и возвращает v * factor.

auto multiplier = [] (int factor)
{
    return [=] (int v) { return v * factor; };
};

auto doubler = multiplier(2);

std::cout << doubler(3) << std::endl; // prints 6

Обратите внимание на шаблон: в итоге мы умножаем два числа, но мы укажем числа в два этапа. Функтор, возвращаемый из вызова multiplier, действует как "пакет", содержащий первое число.

Хотя lambdas относительно новы, они, вероятно, станут очень распространенной частью стиля С++ (как и на любом другом языке, к которому они добавлены).

Но, к сожалению, на данный момент мы достигли "режущей кромки", поскольку приведенный выше пример работает в GCC, но не в MSVC 12 (я не пробовал это в MSVC 13). Тем не менее, он проходит проверку Intellisense MSVC 12 (они используют два совершенно разных компилятора)! И вы можете исправить это, обернув внутреннюю лямбду с помощью std::function<int(int)>( ... ).

Тем не менее, вы можете использовать эти идеи в старой школе С++ при написании функторов вручную.

Глядя вперед, возобновляемые функции могут превратиться в некоторую будущую версию языка (Microsoft настойчиво стремится к ним, поскольку они практически идентичны async/await на С#), и это еще одно размытие различия между функциями и классы (возобновляемая функция действует как конструктор для класса конечной машины).