Как бороться с бессмысленными методами для типов, содержащихся в варианте в C ++ 17 - PullRequest
1 голос
/ 08 февраля 2020

Я имею дело со случаем, когда определенный контейнерный класс должен содержать вариант пользовательских классов (не в последнюю очередь для сбора экземпляров таких классов в векторе). Они в свою очередь связаны друг с другом. В примере кода типы в этом варианте - Bird и Fish, а класс контейнера - AnimalContainer (полный рабочий код см. Ниже).

Неполный класс обзор:

using namespace std;
using uint = unsigned int;

class Animal {      
    protected:
        uint length_;
};

class Fish : public Animal {   
    private:
        uint depths_of_dive_;
};

class Bird : public Animal {   
    private:
        uint wing_span_;
};

class AnimalContainer {
    private:
        variant<Bird, Fish> the_animal_;
};

Теперь (игнорируя пингвинов и некоторых других птиц) , птицы обычно не могут нырять, а у рыб нет крыльев (не слышал любого хотя бы) . Однако код должен предоставлять возможность запрашивать wing_span_ через экземпляр a класса AnimalContainer с использованием a.WingSpan(), если это животное будет Bird, а также depth_of_dive_ с использованием a.DepthOfDive(), это должно быть Fish. Кроме того, для каждого Bird и Fish можно оценить (физиологически нереалистичный c) вес, то есть можно назвать a.EstimatedWeight().

В основном для того, чтобы Во избежание ошибок компилятора, метод WingSpan() добавлен в класс Fi sh, а DepthOfDive() добавлен в класс Bird.

Добавление этих фиктивных методов может стать очень громоздким, особенно когда более двух варианты (здесь Fish и Bird), или когда эти классы содержат много методов.

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

У вас есть предложения, как это сделать в более всеобъемлющий способ, который требует меньше копирования и вставки? Если у вас есть общие проблемы с этой концепцией, советуем также приветствовать.

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

Пример первого рабочего кода

#include <variant>
#include <iostream>

using namespace std;
using uint = unsigned int;


class Animal {
    public:
        Animal(uint length) : length_{length} {}

        uint Length() { return length_; }

    protected:
        uint length_;
};

class Fish : public Animal {
    public:
        Fish(uint length, uint depths_of_dive) : Animal(length), depths_of_dive_{depths_of_dive} {}

        uint DepthOfDive() { return depths_of_dive_; }
        uint EstimatedWeight() { return length_ * length_; }

        uint WingSpan() { cerr << "Usually fishes do not have wings... "; return 0; }

    private:
        uint depths_of_dive_;
};

class Bird : public Animal {
    public:
        Bird(uint length, uint wing_span) : Animal(length), wing_span_{wing_span} {}

        uint WingSpan() { return wing_span_; }
        uint EstimatedWeight() { return wing_span_ * length_; }

        uint DepthOfDive() { cerr << "Usually birds can not dive... "; return 0; }

    private:
        uint wing_span_;
};

class AnimalContainer {
    public:
        AnimalContainer(Bird b) : the_animal_{b} {}
        AnimalContainer(Fish f) : the_animal_{f} {}

        uint Length() {
            return visit([] (auto arg) { return arg.Length(); }, the_animal_);
        }
        uint WingSpan() {
            return visit([] (auto arg) { return arg.WingSpan(); }, the_animal_);
        }
        uint DepthOfDive() {
            return visit([] (auto arg) { return arg.DepthOfDive(); }, the_animal_);
        }
        uint EstimatedWeight() {
            return visit([] (auto arg) { return arg.EstimatedWeight(); }, the_animal_);
        }


    private:
        variant<Bird, Fish> the_animal_;
};

int main()
{
    Fish f(2,3);
    Bird b(2,3);

    AnimalContainer a_1(f);
    AnimalContainer a_2(b);

    cout << a_1.Length() << ' ' << a_1.WingSpan() << ' ' << a_1.DepthOfDive() << ' ' << a_1.EstimatedWeight() << endl; 
    cout << a_2.Length() << ' ' << a_2.WingSpan() << ' ' << a_2.DepthOfDive() << ' ' << a_2.EstimatedWeight() << endl;

    return 0;
}

Пример второго рабочего кода

#include <variant>
#include <iostream>

using namespace std;
using uint = unsigned int;


class Animal {
    public:
        Animal(uint length) : length_{length} {}

        uint Length() { return length_; }

    protected:
        uint length_;
};

class Fish : public Animal {
    public:
        Fish(uint length, uint depths_of_dive) : Animal(length), depths_of_dive_{depths_of_dive} {}

        uint DepthOfDive() { return depths_of_dive_; }
        uint EstimatedWeight() { return length_ * length_; }

        // no more dummy function

    private:
        uint depths_of_dive_;
};

class Bird : public Animal {
    public:
        Bird(uint length, uint wing_span) : Animal(length), wing_span_{wing_span} {}

        uint WingSpan() { return wing_span_; }
        uint EstimatedWeight() { return wing_span_ * length_; }

        // no more dummy function

    private:
        uint wing_span_;
};

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

class AnimalContainer {
    public:
        AnimalContainer(Bird b) : the_animal_{b} {}
        AnimalContainer(Fish f) : the_animal_{f} {}

        uint Length() {
            return visit([] (auto arg) { return arg.Length(); }, the_animal_);
        }
        uint WingSpan() {
            return visit(overloaded { // now overloaded version
                [] (auto) { cerr << "This animal does not have wings... "; return uint(0); },
                [] (Bird arg) { return arg.WingSpan(); }}, the_animal_);
        }
        uint DepthOfDive() {
            return visit(overloaded { // now overloaded version
                [] (auto) { cerr << "This animal can not dive... "; return uint(0); },
                [] (Fish arg) { return arg.DepthOfDive(); }}, the_animal_);
        }
        uint EstimatedWeight() {
            return visit([] (auto arg) { return arg.EstimatedWeight(); }, the_animal_);
        }

    private:
        variant<Bird, Fish> the_animal_;
};

int main()
{
    Fish f(2,3);
    Bird b(2,3);

    AnimalContainer a_1(f);
    AnimalContainer a_2(b);

    cout << a_1.Length() << ' ' << a_1.WingSpan() << ' ' << a_1.DepthOfDive() << ' ' << a_1.EstimatedWeight() << endl; 
    cout << a_2.Length() << ' ' << a_2.WingSpan() << ' ' << a_2.DepthOfDive() << ' ' << a_2.EstimatedWeight() << endl;

    return 0;
}

1 Ответ

1 голос
/ 09 февраля 2020

Прежде всего, позвольте мне сказать, что мне очень приятно видеть хорошо сформулированный вопрос о дизайне от нового участника. Добро пожаловать в StackOverflow! :)

Как вы правильно упомянули, у вас есть два варианта: обрабатывать несуществующее поведение в конкретных классах или в контейнере. Давайте рассмотрим оба варианта.

Конкретные классы

Обычно это делается с помощью наследования и (динамического c) полиморфизма, подход classi c OOP. В этом случае у вас даже не должно быть variant, поскольку variant используется для несвязанных классов. Не имеет смысла использовать его, когда у вас уже есть общий базовый класс.

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

Код может выглядеть следующим образом (не проверено, просто показывает вам концепцию):

// Pure interface on top of the hierarchy
class IAnimal {
    public:
        virtual ~IAnimal() = default.

        virtual uint Length() const = 0;
        virtual uint DepthOfDive() const = 0;
        virtual uint EstimatedWeight() const = 0;
        virtual uint WingSpan() const = 0;
};

// Intermediate class with some common implementations 
class Animal : public IAnimal {
    public:
        Animal(uint length) : length_{length} {}

        // We know how to implement this on this level already, so mark this final
        // Otherwise it won't have much sense to have the length_ field
        uint Length() const final { return length_; }

        // Some of these should be overridden by the descendants
        uint DepthOfDive() const override { cerr << "This creature can not dive... "; return 0; }
        uint WingSpan() const override { cerr << "This creature does not have wings... "; return 0; }

    private:
        uint length_;  // Better make it private
};

class Fish : public Animal {
    public:
        Fish(uint length, uint depths_of_dive) : Animal(length), depths_of_dive_{depths_of_dive} {}

        uint DepthOfDive() const { return depths_of_dive_; }
        uint EstimatedWeight() const { return Length() * Length(); }

    private:
        uint depths_of_dive_;
};

class Bird : public Animal {
    public:
        Bird(uint length, uint wing_span) : Animal(length), wing_span_{wing_span} {}

        uint WingSpan() const { return wing_span_; }
        uint EstimatedWeight() const { return wing_span_ * Length(); }

    private:
        uint wing_span_;
};

using AnimalContainer = std::unique_ptr<IAnimal>;

Теперь вместо объединяющего контейнера вы можете напрямую использовать указатель на базовый интерфейс. Classi c.

Контейнер

Наличие объединяющего контейнера, предоставляющего некоторый общий интерфейс, может иметь смысл, если у вас нет базового класса. В противном случае вам лучше отступить к классу c OOP, описанному выше. Итак, в этом случае вам лучше полностью избавиться от класса Animal и определить, что вам нужно, так, как вам нужно для всех указанных c животных.

Что касается реализации, ваш подход на самом деле довольно хорош, используя причудливый шаблон overloaded. Единственное, что я могу порекомендовать вам рассмотреть, - это использовать одну лямбду generi c в качестве посетителя с кучей if constexpr внутри, так как это может быть легче читать при некоторых обстоятельствах. Но это действительно зависит, и в вашем подходе нет ничего плохого.

...