Дизайн класса - использовать опционально? варианты? быть непрозрачным? - PullRequest
2 голосов
/ 28 февраля 2020

Я хочу иметь класс для расположения шины PCI. Для обсуждения они бывают трех видов:

  • [домен]: [шина]: [устройство]. [Функция]
  • [домен]: [шина]: [устройство]
  • [шина]: [устройство]. [функция]

и, скажем, каждое поле является неотрицательным целым значением (скажем, unsigned только для сделай все просто).

Я ломаю голову над тем, как определить этот класс. Я мог бы использовать std::optional s для полей домена и функции; но тогда они не оба необязательно. Я мог бы использовать вариант с 3 типами, но тогда мне нужно определить отдельные типы, которые сильно перекрывают друг друга. Я мог бы просто хранить 4 unsigned s и 3-значное перечисление, для которого действует формат - но это немного хлопотно, и мне нужен геттер и сделать класс непрозрачным. То же самое, если я попытаюсь как-то использовать объединение.

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

Примечание: Любая языковая стандартная версия подходит для ответа, хотя я сомневаюсь, что C ++ 20 даст вам что-нибудь.

Ответы [ 3 ]

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

Исходя из моего комментария, мне было интересно, может ли что-то подобное сработать:

enum class pci_format { domain_function, domain, function };

template <pci_format E> struct tag { }; 

class pci_location {
  public:
    pci_location (tag<pci_format::domain_function>, unsigned domain, unsigned bus,
      unsigned device, unsigned function)
      : format_(pci_format::domain_function)
      , domain_(domain)
      , bus_(bus)
      , device_(device)
      , function_(function)
    { }

    // Repeat for other values of pci_format.

    pci_format format () const { return format_; }

    bool has_domain () const {
      return (format_ == pci_format::domain_function)
        or (format_ == pci_format::domain);
    }

    unsigned domain () const {
      if (not has_domain()) { throw std::runtime_error("Domain not available."); }
      return domain_;
    }

    // Repeat for other fields.

  private:
    pci_format format_;
    unsigned domain_;
    unsigned bus_;
    unsigned device_;
    unsigned function_
};

В основном вы создадите специфицированный c конструктор для каждого "формата" PCI. Конечно, вы также можете хранить каждый unsigned как std::optional<unsigned>, но это заставит пользователей «разыменовывать» каждый необязательный параметр, даже если они точно знали, что он должен содержать значение.

Так или иначе Им придется проверить, в каком «формате» находится местоположение, поэтому мне кажется, что использование enum для этого более удобно для пользователя. Тогда пользователям нужно только проверить один раз и точно знать, какие поля доступны.

Я полагаю, что вы могли бы поместить посетителя поверх всего этого, чтобы они могли просто предоставить код для выполнения для каждого "формата":

struct pci_location_visitor {
  virtual void visit (tag<pci_format::domain_function>, pci_location const & obj) = 0;
  // Repeat for other enum values.
};

// Add to pci_location:
void accept (pci_location_visitor & visitor) {
  switch (format_) {
    case pci_format::domain_function:
      return visitor.visit(tag<pci_format::domain_function>{}, *this);
    default: throw std::runtime_error("Format not supported for visitation.");
  }
}

Затем вы можете создать посетителя, который может быть построен из набора вызываемых элементов, то есть лямбд, так что все это можно использовать, как показано ниже:

pci_location const & loc = getIt();
auto printSomething = make_pci_location_visitor(
      [](tag<pci_format::domain_function>, pci_location const & e) { std::cout << e.domain(); }
    , [](tag<pci_format::domain>,          pci_location const & e) { std::cout << e.bus(); }
    , [](tag<pci_format::function>,        pci_location const & e) { std::cout << e.function(); }
  );
loc.accept(printSomething);

Для пример того, как можно создать такого посетителя, см. класс overloaded в примере std::visit на cppreference.com .

0 голосов
/ 28 февраля 2020

Я бы сделал и домен, и функцию необязательными (мне все равно, насколько это эффективно, если только это эффективно), и я бы просто применял условие единственного отсутствия в качестве инварианта класса. Таким образом, только функции, которые могут изменять любое из полей, должны выполнять проверку и сообщать пользователю о возможных ошибках. Не нужно раздувать ваш код вариантами или динамически интерпретируемыми unsigned int массивами. KISS.

0 голосов
/ 28 февраля 2020

Как просили в комментариях ... учитывая, что у меня нет особых требований к тому, как пользователи предпочли бы использовать этот класс, учитывая C ++ 14, я буду делать что-то общее c в соответствии с:

#include <array>
#include <climits>
#include <iostream>
#include <stdexcept>

class pci_location_t {
public:
    struct dbdf {
        unsigned int domain;
        unsigned int bus;
        unsigned int device;
        unsigned int function;
    };
    struct dbd {
        unsigned int domain;
        unsigned int bus;
        unsigned int device;
    };
    struct bdf {
        unsigned int bus;
        unsigned int device;
        unsigned int function;
    };

    pci_location_t(dbdf v) : domain(v.domain), bus(v.bus), device(v.device), function(v.function) {}
    pci_location_t(dbd v) : domain(v.domain), bus(v.bus), device(v.device), function(INVALID) {}
    pci_location_t(bdf v) : domain(INVALID), bus(v.bus), device(v.device), function(v.function) {}

    template <typename dbdf_f, typename dbd_f, typename bdf_f>
    auto visit(dbdf_f dbdf_fun, dbd_f dbd_fun, bdf_f bdf_fun) const {
        if (domain == INVALID) {
            if (function == INVALID) {
                throw std::domain_error("Wrong PCI location format");
            }
            return bdf_fun(bdf{bus, device, function});
        } else if (function == INVALID) {
            return dbd_fun(dbd{domain, bus, device});
        } else {
            return dbdf_fun(dbdf{domain, bus, device, function});
        }
    }

private:
    friend pci_location_t invalid_location();
    pci_location_t() : domain(INVALID), bus(INVALID), device(INVALID), function(INVALID) {}

    const static unsigned int INVALID = UINT_MAX;

    unsigned int domain;
    unsigned int bus;
    unsigned int device;
    unsigned int function;
};

pci_location_t invalid_location() { return pci_location_t{}; }

int main() {
    std::array<pci_location_t, 4> locations = {
        pci_location_t(pci_location_t::dbdf{1, 2, 3, 4}),
        pci_location_t(pci_location_t::dbd{1, 2, 3}),
        pci_location_t(pci_location_t::bdf{2, 3, 4}),
        invalid_location()
    };
    try {
        for (auto& l : locations) {
            l.visit(
                [] (auto dbdf) {
                    std::cout << dbdf.domain << ":" << dbdf.bus << ":" << dbdf.device << "." << dbdf.function << std::endl;
                },
                [] (auto dbd) {
                    std::cout << dbd.domain << ":" << dbd.bus << ":" << dbd.device << std::endl;
                },
                [] (auto bdf) {
                    std::cout << bdf.bus << ":" << bdf.device << "." << bdf.function << std::endl;
                }
            );
        }
        std::cout << "Done!" << std::endl;
    } catch(const std::exception& e) {
        std::cout << e.what() << std::endl;
    }
    return 0;
}

(вы можете проверить его на Coliru ).

Не стесняйтесь использовать дополнительные параметры или отдельное поле формата, если вам не нравятся специальные значения.

...