Anonymous Entity
Anonymous Entity

Reputation: 3350

C++ function argument safety

In a function that takes several arguments of the same type, how can we guarantee that the caller doesn't mess up the ordering?

For example

void allocate_things(int num_buffers, int pages_per_buffer, int default_value ...

and later

// uhmm.. lets see which was which uhh..
allocate_things(40,22,80,...

Upvotes: 58

Views: 3965

Answers (7)

Nir Friedman
Nir Friedman

Reputation: 17704

Two good answers so far, one more: another approach would be to try leverage the type system wherever possible, and to create strong typedefs. For instance, using boost strong typedef (http://www.boost.org/doc/libs/1_61_0/libs/serialization/doc/strong_typedef.html).

BOOST_STRONG_TYPEDEF(int , num_buffers);
BOOST_STRONG_TYPEDEF(int , num_pages);

void func(num_buffers b, num_pages p);

Calling func with arguments in the wrong order would now be a compile error.

A couple of notes on this. First, boost's strong typedef is rather dated in its approach; you can do much nicer things with variadic CRTP and avoid macros completely. Second, obviously this introduces some overhead as you often have to explicitly convert. So generally you don't want to overuse it. It's really nice for things that come up over and over again in your library. Not so good for things that come up as a one off. So for instance, if you are writing a GPS library, you should have a strong double typedef for distances in metres, a strong int64 typedef for time past epoch in nanoseconds, and so on.

Upvotes: 30

sergej
sergej

Reputation: 17999

If you have a C++11 compiler, you could use user-defined literals in combination with user-defined types. Here is a naive approach:

struct num_buffers_t {
    constexpr num_buffers_t(int n) : n(n) {}  // constexpr constructor requires C++14
    int n;
};

struct pages_per_buffer_t {
    constexpr pages_per_buffer_t(int n) : n(n) {}
    int n;
};

constexpr num_buffers_t operator"" _buffers(unsigned long long int n) {
    return num_buffers_t(n);
}

constexpr pages_per_buffer_t operator"" _pages_per_buffer(unsigned long long int n) {
    return pages_per_buffer_t(n);
}

void allocate_things(num_buffers_t num_buffers, pages_per_buffer_t pages_per_buffer) {
    // do stuff...
}

template <typename S, typename T>
void allocate_things(S, T) = delete; // forbid calling with other types, eg. integer literals

int main() {
    // now we see which is which ...
    allocate_things(40_buffers, 22_pages_per_buffer);

    // the following does not compile (see the 'deleted' function):
    // allocate_things(40, 22);
    // allocate_things(40, 22_pages_per_buffer);
    // allocate_things(22_pages_per_buffer, 40_buffers);
}

Upvotes: 30

Walter
Walter

Reputation: 45444

Just for completeness, you could use named arguments, when your call becomes.

void allocate_things(num_buffers=20, pages_per_buffer=40, default_value=20);
// or equivalently
void allocate_things(pages_per_buffer=40, default_value=20, num_buffers=20);

However, with the current C++ this requires quite a bit of code to be implemented (in the header file declaring allocate_things(), which must also declare appropriate external objects num_buffers etc providing operator= which return a unique suitable object).

---------- working example (for sergej)

#include <iostream>

struct a_t { int x=0; a_t(int i): x(i){} };
struct b_t { int x=0; b_t(int i): x(i){} };
struct c_t { int x=0; c_t(int i): x(i){} };

// implement using all possible permutations of the arguments.
// for many more argumentes better use a varidadic template.
void func(a_t a, b_t b, c_t c)
{ std::cout<<"a="<<a.x<<" b="<<b.x<<" c="<<c.x<<std::endl; }
inline void func(b_t b, c_t c, a_t a) { func(a,b,c); }
inline void func(c_t c, a_t a, b_t b) { func(a,b,c); }
inline void func(a_t a, c_t c, b_t b) { func(a,b,c); }
inline void func(c_t c, b_t b, a_t a) { func(a,b,c); }
inline void func(b_t b, a_t a, c_t c) { func(a,b,c); }

struct make_a { a_t operator=(int i) { return {i}; } } a;
struct make_b { b_t operator=(int i) { return {i}; } } b;
struct make_c { c_t operator=(int i) { return {i}; } } c;

int main()
{
  func(b=2, c=10, a=42);
}

Upvotes: 7

Dan Haynes
Dan Haynes

Reputation: 388

Are you really going to try to QA all the combinations of arbitrary integers? And throw in all the checks for negative/zero values etc?

Just create two enum types for minimum, medium and maximum number of buffers, and small medium and large buffer sizes. Then let the compiler do the work and let your QA folks take an afternoon off:

allocate_things(MINIMUM_BUFFER_CONFIGURATION, LARGE_BUFFER_SIZE, 42);

Then you only have to test a limited number of combinations and you'll have 100% coverage. The people working on your code 5 years from now will only need to know what they want to achieve and not have to guess the numbers they might need or which values have actually been tested in the field.

It does make the code slightly harder to extend, but it sounds like the parameters are for low-level performance tuning, so twiddling the values should not be perceived as cheap/trivial/not needing thorough testing. A code review of a change from allocate_something(25, 25, 25);

...to

allocate_something(30, 80, 42);

...will likely get just a shrug/blown off, but a code review of a new enum value EXTRA_LARGE_BUFFERS will likely trigger all the right discussions about memory use, documentation, performance testing etc.

Upvotes: 6

chux
chux

Reputation: 154280

(Note: post was originally tagged 'C`)

C99 onwards allows an extension to @Dietrich Epp idea: compound literal

struct things {
  int num_buffers;
  int pages_per_buffer;
  int default_value 
};
allocate_things(struct things);

// Use a compound literal
allocate_things((struct things){.default_value=80, .num_buffers=40, .pages_per_buffer=22});

Could even pass the address of the structure.

allocate_things(struct things *);

// Use a compound literal
allocate_things(&((struct things){.default_value=80,.num_buffers=40,.pages_per_buffer=22}));

Upvotes: 9

Frank Puffer
Frank Puffer

Reputation: 8215

You can't. That's why it is recommended to have as few function arguments as possible.

In your example you could have separate functions like set_num_buffers(int num_buffers), set_pages_per_buffer(int pages_per_buffer) etc.

You probably have noticed yourself that allocate_things is not a good name because it doesn't express what the function is actually doing. Especially I would not expect it to set a default value.

Upvotes: 7

Dietrich Epp
Dietrich Epp

Reputation: 213528

A typical solution is to put the parameters in a structure, with named fields.

AllocateParams p;
p.num_buffers = 1;
p.pages_per_buffer = 10;
p.default_value = 93;
allocate_things(p);

You don't have to use fields, of course. You can use member functions or whatever you like.

Upvotes: 68

Related Questions