Reputation: 375
How can I avoid mistakes with passing parameters of same type to function?
Let's consider function reading some binary data:
std::vector<uint8_t> read(size_t offset, size_t amount);
It's so easy to mistake offset with amount(I did similar it many times).
I see solution to this:
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);
Is there a better solution to avoid mistakes like that?
Upvotes: 4
Views: 2059
Reputation: 5668
There are two approaches I can think of.
This is essentially what you are suggesting in your question, but I would implement it generically.
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);
This allows you to expand the same concept to other underlying data types.
The Named Parameter Idiom is somewhat similar to the Options
approach in @PaulBelanger's answer, but it can be used in place and doesn't allow the user to take the the curly-brace shortcut that brings you back to the same problem you had before. However, it will default-initialize all your parameters, so while you are protected from mixing up parameters, it can't force you to provide explicit values for all of them. For your example:
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
}
You could implement the members of ReadParams
as std::optional
s and throw a runtime error if an uninitialized member is access; but you can no longer enforce at compile time that the user actually provides all parameters.
Upvotes: 1
Reputation: 2434
Another thing that you can do is to pass parameters in a structure. This also allows you to set sensible defaults for the values. This is especially useful when a constructor takes a large number of arguments. For example:
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);
// ...
};
Which is then used like:
FooService::Options o;
o.port = 1338;
//all other values keep their defaults.
auto service = make_unique<FooService>(o);
Upvotes: 1