MamCieNaHita
MamCieNaHita

Reputation: 375

How to avoid mistakes in functions with same type arguments

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

Answers (2)

mindriot
mindriot

Reputation: 5668

There are two approaches I can think of.

Tagged Types

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.

Named Parameter Idiom

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::optionals 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

Paul Belanger
Paul Belanger

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

Related Questions