Разъединение классов хоста и политики за счет классов политики с сохранением состояния и без соблюдения пункта 26 действующего C++

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

ОПИСАНИЕ ПРОБЛЕМЫ

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

template <typename FooPolicy> 
struct Alg {
    void operator()() {
        ...
        FooPolicy::foo(arguments);
        ...
    }
};

Существует определенная связь между указанным выше классом хоста и политикой: если подпись FooPolicy::foo изменения, то код в Alg::operator() должен измениться соответственно.

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

template <typename FooPolicy> struct Alg {
    void operator()() {
        int arg = 5; // any computation can be here
        // using tag dispatch to call the correct `foo`
        foo(std::integral_constant<bool, FooPolicy::paramFlag>{}, arg);
    }

private:
    void foo(std::true_type, int param) { FooPolicy::foo(param); }
    void foo(std::false_type, int param) {
        (void)param;
        FooPolicy::foo();
    }
};

struct SimpleFoo {
    static constexpr bool paramFlag = false;
    static void foo();
};

struct ParamFoo {
    static constexpr bool paramFlag = true;
    static void foo(int param);
};

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

ПРЕДЛАГАЕМЫЙ ДИЗАЙН

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

// The policy-independent part of Alg factored out
struct AlgPolicyIndependent {
    int getArg() const { return arg; }
protected:
    int arg;
};

// The interface to be used by FooPolicy
struct FooPolicyServices {
    FooPolicyServices(const AlgPolicyIndependent &myAlg) : alg(myAlg) {};
    int getArg() const { return alg.getArg(); }
private:
    const AlgPolicyIndependent &alg;
};

template <typename FooPolicy>
struct Alg : private AlgPolicyIndependent {
    Alg() : fooPolicy(FooPolicyServices(*this)) {};
    void operator()() {
        arg = 5; // any computation can be here
        fooPolicy.foo();
    }
private:
    FooPolicy fooPolicy;
};

struct SimpleFoo {
    SimpleFoo(const FooPolicyServices &myS) : s(myS) {};
    void foo() { std::cout << "In SimpleFoo" << std::endl; }
private:
    const FooPolicyServices &s;
};

struct ParamFoo {
    ParamFoo(const FooPolicyServices &myS) : s(myS) {};
    void foo() { std::cout << "In ParamFoo " << s.getArg() << std::endl; }
private:
    const FooPolicyServices &s;
};

АНАЛИЗ

При таком дизайне политика может свободно использовать любые данные, которые можно получить с помощью открытого интерфейса соответствующего Services учебный класс. В нашем примере ParamFoo::foo получил алгоритм arg используя FooPolicyServices::getArg, Хост-класс просто вызывает FooPolicy::foo без аргументов, и это не должно измениться, даже если FooPolicy::foo изменения, который является разделением, которое мы хотели.

Я вижу два недостатка этого дизайна:

  1. arg стал частью состояния Alg вместо того, чтобы быть локальной переменной в Alg::operator(), что противоречит пункту 26 действующего C++, в котором говорится, что переменные должны быть определены как можно позже. Тем не менее, аргументация этого пункта не применяется, если стоимость дополнительной инициализации arg незначительно по сравнению со стоимостью запуска алгоритма.

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

ВОПРОСЫ

Три вопроса:

  1. Стоит ли разделение, достигаемое предложенной конструкцией, двумя недостатками, перечисленными выше?

  2. Есть ли недостатки, которые я упустил из виду?

  3. Есть ли у предложенного дизайна имя?


Основываясь на ответе @Useless, вот обновленная реализация. Эта реализация позволяет иметь классы политики без сохранения состояния, но имеет дополнительные издержки на передачу той же ссылки на Services объект каждый раз, когда используется политика.

// The policy-independent part of Alg
struct AlgPolicyIndependent {
    int getArg() const { return arg; }
protected:
    int arg;
};

// The interface to be used by FooPolicy
struct FooPolicyServices {
    FooPolicyServices(const AlgPolicyIndependent &myAlg) : alg(myAlg) {};
    int getArg() const { return alg.getArg(); }
private:
    const AlgPolicyIndependent &alg;
};

template <typename FooPolicy>
struct Alg : private AlgPolicyIndependent {
    Alg() : fooPolicyServices(*this) {};
    void operator()() {
        arg = 5; // any computation can be here
        FooPolicy::foo(fooPolicyServices);
    }
private:
    FooPolicyServices fooPolicyServices;
};

struct SimpleFoo {
    static void foo(const FooPolicyServices &s) {
        (void)s;
        std::cout << "In SimpleFoo" << std::endl;
    }
};

struct ParamFoo {
    static void foo(const FooPolicyServices &s) {
        std::cout << "In ParamFoo " << s.getArg() << std::endl;
    }
};

1 ответ

Существует определенная связь между указанным выше классом хоста и политикой

Или "Связывание, присущее шаблону Политики, является проблемой"?

Нет. Существует эквивалентная степень связи между каждым из алгоритмов и политикой, и интерфейсом, который один требует и другой реализует.

Рассмотрим динамически-полиморфный эквивалент Стратегии:

struct IStrategy {
    virtual ~IStrategy() {}
    virtual void foo() = 0;
};
struct FooStrategy: public IStrategy {
    void foo() override;
}
void algo(IStrategy *s) {
    // ...
    s->foo();
}

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

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

Обратите внимание, что есть точная аналогия времени выполнения для вашего варианта - опционально реализуя другие (и более конкретные) интерфейсы, и используя dynamic_cast исследовать во время выполнения, которые поддерживаются.


Вопросы

  1. Стоит ли разделять... стоит двух недостатков, перечисленных выше?

    Может быть! Это зависит от того, насколько проблематичным стало взаимодействие на практике, и сколько состояния передается.

    В любом случае...

  2. Есть ли недостатки, которые я упустил из виду?

    Есть много недостатков в отношении состояния в целом, и в частности, предлагаемого вами проекта:

    • глобальное (т.е. не локальное) состояние плохо взаимодействует с параллелизмом (используйте один и тот же объект алгоритма из нескольких потоков, и все разрывается)
    • он плохо взаимодействует с повторным использованием (случайно использует один и тот же объект алгоритма из своего собственного вызова Policy, и все ломается)
    • если вы хотите повторно использовать объект с состоянием, вам может потребоваться предоставить дополнительный метод для сброса его состояния
    • Затем вы должны помнить, чтобы вызывать этот метод везде, где объект может быть повторно использован
    • две вышеприведенные точки по сути являются плохой, ручной, подверженной ошибкам эмуляцией простого использования состояния локальной области в первую очередь.
    • в дополнение к написанию этого дополнительного кода, вы также сделали свой объект больше, чтобы сохранить состояние, которое вы действительно не хотите вне области метода в любом случае
    • в вашем конкретном случае вы также увеличили политику (чтобы сохранить указатель на свое состояние), и каждый вызов политики теперь имеет дополнительную косвенность в погоне за указателем this->services->value получить аргумент

    Хотя это проблемы, их также можно полностью избежать. Просто передайте объект контекста локальной области методу policy при каждом вызове. Затем:

    • нет никакого требования для объекта algo вообще быть состоящим из состояния, потому что у объекта контекста есть локальная область действия в методе
    • нет необходимости в том, чтобы политика была с состоянием, потому что контекст может быть просто передан в качестве единственного аргумента при каждом вызове

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

  3. Есть ли у предложенного дизайна имя?

    Это выглядит как шаблон контекста для меня. Исходное предложение выглядит как своего рода неудобное внедрение зависимостей, но я бы придерживался политики и алгоритма без сохранения состояния и передавал рабочее состояние в объекте Context для предпочтения.

Другие вопросы по тегам