Интерфейс для перемещения контейнера, который представляет собой класс переменную-член


Я часто классы, дизайн которых модель объекта, который имеет несколько подмодулей того же типа. Например, у меня есть класс на печатной плате (PCB), которая имеет несколько подсхем (я буду называть их "порты"). Класс для текстолита (Board) использует контейнер (например, std::vector) в качестве члена переменную для хранения подсхемы. Клиентский код должен быть в состоянии перебрать порты для запроса состояния каждого порта, управление порта, и т. д., Поэтому мне нужно, чтобы обеспечить итераторы для портов.

В общем совет может иметь несколько схем различных типов (и, следовательно, несколько контейнеров-членов, которые должны будут обрабатываться). Например, Board класс может иметь std::vector<Port> для портов, std::vector<double> для сведения шине напряжения сигнала и т. д. Таким образом, я не могу просто представить begin() и end() функции-члены -- я должен называть их как-то так begin_ports() и end_ports() дифференцировать контейнер Port объекты из других членов контейнеры. Такие нестандартные имена помешает мне использовать циклы на основе диапазона (которые зависят от функции по имени begin() и end()).

Я искал вокруг на программной инженерии SE и переполнения стека, чтобы увидеть, если есть стандартное решение для этого, и я нашел пару интересных предложений создать класс-контейнер, который в частном порядке наследует от соответствующих стандартный контейнер (например, здесь и здесь) и делает функции средств доступа, как begin()/end() общественности. Однако, это, кажется, идти против Советов в пользу композицию наследованию (например, здесь и здесь), когда состав можно.

Ниже приводится решение для Board класс, который имеет несколько Port объекты, хранящиеся в контейнере, называемом Ports что использование композиции вместо наследования. Board и Port упрощены с фактическим кодом, и для этого обзора, я хотел бы сосредоточиться в основном на Ports класс контейнера и соответствующие функции средств доступа к ней в Board. (Обратите внимание, что первый порт имеет номер 1, а не 0, так как порты нумеруются на схеме физического совета.)

Доска.ч

#ifndef BOARD_H
#define BOARD_H

#include <vector>

class Board {
public:
    class Port {
    private:
        bool p_enabled;

        double p_vout;
    public:
        Port();

        auto is_enabled() const {
            return p_enabled;
        }

        bool enable();

        bool disable();

        auto v_out() const {
            return p_vout;
        }

        bool v_out(double v);

        bool reset();
    }; // end class Port

    class Ports {
        // Every Ports container is owned by a Board so Board has access to its Ports
        // container's private members and functions.
        friend class Board;

        typedef std::vector<Port> container_type;

        container_type ports_;

        // Default-constructs a specified number of Board::Ports.
        // The constructor is private so only a Board can construct a Ports container.
        Ports(container_type::size_type count) : ports_(container_type(count)) {}

        // Deleted copy constructor.
        // An instance of the class cannot be copied from a reference to it.
        // This means the return value of Board::ports() can only be assigned to a reference.
        Ports(const Ports& other) = delete;
    public:
        typedef container_type::size_type size_type;
        typedef container_type::iterator iterator;
        typedef container_type::const_iterator const_iterator;

        const Port& operator[](size_type index) const;
        Port& operator[](size_type index);

        const Port& at(size_type index) const;
        Port& at(size_type index);

        iterator begin();
        const_iterator begin() const;
        const_iterator cbegin() const;

        iterator end();
        const_iterator end() const;
        const_iterator cend() const;
    };
private:
    Ports m_ports;

    double vsupply;
public:
    typedef Ports::size_type port_size_type;
    typedef Ports::iterator port_iterator;
    typedef Ports::const_iterator const_port_iterator;

    Board(double vsupply, port_size_type num_ports);

    const Ports& ports() const {
        return m_ports;
    }

    Ports& ports() {
        return m_ports;
    }

    const Port& port(port_size_type index) const {
        return m_ports[index];
    }

    Port& port(port_size_type index) {
        return m_ports[index];
    }

    const Port& port_at(port_size_type index) const {
        return m_ports.at(index);
    }

    Port& port_at(port_size_type index) {
        return m_ports.at(index);
    }

    double Vsupply() const {
        return vsupply;
    }

    bool Vsupply(double v_pwr);

    /** Resets every Port to its default settings.
    Returns true if any settings were changed on any Port, false otherwise. */
    bool reset();
}; // end class Board

#endif

Board.cpp

#include <stdexcept> // std::invalid_argument

#include "board.h"

Board::Board(double vsupply, port_size_type num_ports) : vsupply(vsupply), m_ports(num_ports) {
    if (vsupply < 0) throw std::invalid_argument("Vsupply < 0V is not allowed");
}

Board::Port::Port() :
    p_enabled(false),
    p_vout(0) {}

bool Board::Port::enable() {
    bool update = (p_enabled == false);
    p_enabled = true;
    return update;
}

bool Board::Port::disable() {
    bool update = (p_enabled == true);
    p_enabled = false;
    return update;
}

bool Board::Port::v_out(double v) {
    bool update = (p_vout != v);
    p_vout = v;
    return update;
}

bool Board::Port::reset() {
    bool update = false;

    update |= disable();
    update |= v_out(0);

    return update;
}

const Board::Port& Board::Ports::operator[](size_type index) const {
    return ports_[index - 1];
}

Board::Port& Board::Ports::operator[](size_type index) {
    return ports_[index - 1];
}

const Board::Port& Board::Ports::at(size_type index) const {
    return ports_.at(index - 1);
}

Board::Port& Board::Ports::at(size_type index) {
    return ports_.at(index - 1);
}

Board::Ports::iterator Board::Ports::begin() {
    return ports_.begin();
}

Board::Ports::const_iterator Board::Ports::begin() const {
    return ports_.begin();
}

Board::Ports::const_iterator Board::Ports::cbegin() const {
    return ports_.cbegin();
}

Board::Ports::iterator Board::Ports::end() {
    return ports_.end();
}

Board::Ports::const_iterator Board::Ports::end() const {
    return ports_.end();
}

Board::Ports::const_iterator Board::Ports::cend() const {
    return ports_.cend();
}

bool Board::Vsupply(double v_supply) {
    if (v_supply < 0) return false;

    vsupply = v_supply;
    return true;
}

bool Board::reset() {
    bool update = false;

    for (auto port : m_ports) {
        update |= port.reset();
    }

    return update;
}

Клиентский код может получить ссылку на Ports член для перебора и/или вызвать ее operator[] и at функции-члены. Клиентский код может также получить доступ к Port непосредственно из Board используя Board::port() или Board::port_at().

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

#include <iostream>
#include <fstream>
#include <algorithm>

#include "board.h"

int main() {
    std::ofstream ofs("demo.txt");
    ofs << std::boolalpha;

    Board b(30, 8);
    const Board& bref = b;

    auto& ports = b.ports();
    auto& cports = bref.ports();

    double v = 0;
    int port_number = 1;

    ofs << "Vsupply = " << bref.Vsupply() << "V\n";
    b.Vsupply(25);
    ofs << "Vsupply = " << b.Vsupply() << "V\n\n";

    for (auto& port : b.ports()) {
        port.enable();
        port.v_out(v);

        v += 2.5;
        port_number++;
    }

    ports[5].reset();

    for (auto& port : cports) {
        ofs << "Vout = " << port.v_out() << "V\n";
        ofs << "enabled?: " << port.is_enabled() << '\n';

        ofs << '\n';
    }

    ofs << "All ports are enabled?: "
        << std::all_of(ports.cbegin(), ports.cend(), [](const auto& p) { return p.is_enabled(); })
        << '\n';

    return 0;
}

Демо-выходы программы:

Vsupply = 30V
Vsupply = 25V

Vout = 0V
enabled?: true

Vout = 2.5V
enabled?: true

Vout = 5V
enabled?: true

Vout = 7.5V
enabled?: true

Vout = 0V
enabled?: false

Vout = 12.5V
enabled?: true

Vout = 15V
enabled?: true

Vout = 17.5V
enabled?: true

All ports are enabled?: false

Мне нравится, как это получилось, несмотря на то, что есть некоторые дополнительные типирования по сравнению с частным наследство решение:1

class Ports : private std::vector<Port> {
    friend class Board;
    public:
        Ports(std::vector<Port>::size_type count) : std::vector<Port>(count) {}

        using std::vector<Port>::begin;
        using std::vector<Port>::cbegin;
        using std::vector<Port>::end;
        using std::vector<Port>::cend;
        using std::vector<Port>::operator[];
};

Может мое решение быть улучшено? Есть ли в C++11 или C++14 я забыл использовать, что бы улучшить его (я компиляции с помощью Visual студии 2017)? Имя Ports для Port контейнер хороший выбор и не слишком похожие на объекты, которые он содержит?

Кроме того, я сделал Ports конструктор приватным и удалил конструктор копирования, чтобы попробовать, чтобы убедиться, что только Board можно построить Ports и ничто не может скопировать один -- есть ли другие способы, чтобы построить или скопировать Ports что я пропустил? Это хорошая идея, чтобы предотвратить строительство/копии или это излишне ограничительным?


1 Когда я играл с частным наследования решение в демонстрационной программы main() я обнаружил, что IntelliSense отображает все базового контейнера функций-членов, несмотря на то, что большинство из них недоступны:

enter image description here

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



167
5
задан 31 января 2018 в 04:01 Источник Поделиться
Комментарии
1 ответ

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

Сначала некоторые замечания о функции C++11/14 вы могли бы использовать:

Данные инициализации: начиная с C++11 (по крайней мере, если я правильно помню) можно указать значения по умолчанию для нестатические данные-члены внутри класса. Например:

class Port {
private:
bool p_enabled = false;
double p_vout = 0;
public:
Port();
// ...
};

Это гораздо более читабельным, чем то, что вы делаете, членов определение в .cpp файл. Другой возможностью является встроенный конструктор со списком инициализации член, который уже был доступен с C++98.

с помощью директивы: вы можете заменить typedefС помощью usingС. Это не изменит многое в ваш код, но это легче читать, более гибкими, и typedefдолжны постепенно исчезать в любом случае.

Сейчас на упрощение: я считаю, что вы должны сделать выбор между разоблачив всю контейнерных портов и выставляя только некоторые способы, чтобы получить доступ к его содержимому. Нет никакого роста в предлагая begin, end, [] и так далее, если клиент уже имеет доступ к vector внутри, так как они могут вызвать эти функции сами, например, с class.container().begin(). И недостатком это понятно, поскольку вы должны сохранять много прозрачных функций, за несколько слоев.

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

3
ответ дан 1 февраля 2018 в 09:02 Источник Поделиться