Как избежать ошибок в функциях с аргументами одного типа
Как я могу избежать ошибок при передаче параметров одного типа в функцию?
Давайте рассмотрим функцию чтения некоторых двоичных данных:
std::vector<uint8_t> read(size_t offset, size_t amount);
Это так легко перепутать с количеством (я делал это много раз).
Я вижу решение этого:
struct Offset
{
explicit Offset(size_t value) : value{value}{}
size_t value;
};
struct Amount
{
explicit Amount(size_t value) : value{value}{}
size_t value;
};
std::vector<uint8_t> read(Offset offset, Amount amount);
Есть ли лучшее решение, чтобы избежать подобных ошибок?
2 ответа
Есть два подхода, которые я могу придумать.
Помеченные типы
По сути, это то, что вы предлагаете в своем вопросе, но я бы реализовал это в общих чертах.
template <typename Tag, typename T>
struct Tagged
{
explicit Tagged(const T& value) : value{value} { }
T value;
};
template <typename Tag, typename T>
Tagged<Tag, T> tag(const T& value)
{
return Tagged<Tag, T>{value};
}
struct OffsetTag
{ };
struct AmountTag
{ };
using Offset = Tagged<OffsetTag, std::size_t>;
using Amount = Tagged<AmountTag, std::size_t>;
std::vector<uint8_t> read(Offset offset, Amount amount);
Это позволяет расширить ту же концепцию на другие базовые типы данных.
Идиома Именованного Параметра
Идиома Именованного Параметра несколько похожа на Options
подход в ответе @PaulBelanger, но он может быть использован на месте и не позволяет пользователю использовать ярлык с фигурными скобками, который возвращает вас к той же проблеме, что и раньше. Тем не менее, он по умолчанию инициализирует все ваши параметры, поэтому, хотя вы защищены от смешения параметров, он не может заставить вас предоставить явные значения для всех них. Для вашего примера:
class ReadParams
{
public:
ReadParams() : m_offset{0}, m_amount{128}
{ }
ReadParams& offset(std::size_t offset)
{
m_offset = offset;
return *this;
}
// Could get rid of this getter if you can make the users
// of this class friends.
std::size_t offset() const { return m_offset; }
ReadParams& amount(std::size_t amount)
{
m_amount = amount;
return *this;
}
// Could get rid of this getter if you can make the users
// of this class friends.
std::size_t amount() const { return m_amount; }
private:
std::size_t m_offset;
std::size_t m_amount;
};
std::vector<uint8_t> read(const ReadParams& params);
int main()
{
read(ReadParams{}.offset(42).amount(2048)); // clear parameter names
// read(ReadParams{42, 2048}); // won't compile
read(ReadParams{}.offset(42)); // also possible, amount uses default value
}
Вы могли бы реализовать членов ReadParams
как std::optional
s и выдает ошибку времени выполнения, если неинициализированный член имеет доступ; но во время компиляции вы больше не можете принудительно установить, что пользователь фактически предоставляет все параметры.
Еще одна вещь, которую вы можете сделать, это передать параметры в структуре. Это также позволяет вам установить разумные значения по умолчанию для значений. Это особенно полезно, когда конструктор принимает большое количество аргументов. Например:
class FooService
{
public:
// parameters struct.
struct Options
{
ip::address listen_address = ip::address::any();
uint16_t port = 1337;
bool allow_insecure_requests = false;
std::string base_directory = "/var/foo/"
};
//constructor takes the options struct to pass values.
explicit FooService(FooServiceOptions options);
// ...
};
Который затем используется как:
FooService::Options o;
o.port = 1338;
//all other values keep their defaults.
auto service = make_unique<FooService>(o);