2

The objective is to help code readability for other programmers.

When creating a function prototype, I tend to put simple types (int, char) directly into the prototype, while more complex structures are generally passed as pointers (struct complex *).

Of course, this is "in general", and there are some obvious counter example : if a simple parameter must be modified by the function, pass a pointer to it instead.

But now, suppose that, among my function parameters, I get a complex but not-so-huge structure to pass, in read only mode (it won't be modified). Up to now, I would do something like this :

int myFunction1(int var1, const complexStruct* var2Ptr);

and of course it works. But now, I am wondering, since the content of the structure won't be modified anyway, would it be better to define such a function prototype instead :

int myFunction2(int var1, complexStruct var2);

It's difficult to tell. My guts tell me myFunction2 is probably easier to read, hence to use, notably by novice programmers, which tend to dislike pointers. Seasoned programmers will probably not mind.

I also guess that the amount of data copied to the stack as part of parameter list will be higher for myFunction2 (assuming myFunction2 is not automatically inlined by the compilator). Of course, as long as complexStruct is not too large, it should not be an issue. But it means the size of the struct is a decision factor.

Last, I guess that myFunction2 might be easier to inline by a compiler. But that one is just a wild guess.

Anyway, since both function prototypes look equivalent, I'm a bit clueless as to which one might be preferable, and wonder if there are other considerations, potentially more important, to factor in the decision.

Summary of comments received so far :

In a nutshell, prefer const struct* under most circumstances.

9
  • 1
    @FilipeGonçalves: You don't need a const, because it's passed by value. Commented Sep 27, 2014 at 12:26
  • 1
    @OliverCharlesworth I know, but from the moment you omit const, someone reading the code cannot be sure that the function will access var2 for read-only access. Even if it's passed by value, const would self-document the code. It's useless, but it improves readability. Commented Sep 27, 2014 at 12:31
  • 5
    @FilipeGonçalves: That doesn't make any sense; by definition, pass-by-value can't modify the original, so the reader can always be "sure". So adding const to the function declaration is utterly superfluous. (However, I strongly advocate putting const in the function definition whenever possible.) Commented Sep 27, 2014 at 12:32
  • 2
    const for a pass by value should cause a compiler error/warning if someone were to change a struct member by accident or due to insufficient knowledge of the function even though the original data in the caller is not modified. In other words it provides a way for someone to know that they have modified something that was not intended to be modified. This behavior may or may not be useful however it does provide a check for programmer error. Commented Sep 27, 2014 at 12:40
  • 2
    @RichardChambers: In the function definition, sure. But it doesn't help at all in the declaration. Commented Sep 27, 2014 at 12:43

1 Answer 1

1

Especially for functions that “belong” to a certain data type, I prefer passing a pointer as the first argument (like the this pointer in C++). It also makes the signatures of mutating and non-mutating functions more consistent.

Consider a struct stack with a stack_push, stack_pop (both mutating) and a (non-mutating) stack_top function. The type might be as small as

struct stack
{
  size_t capacity_;
  size_t size_;
  char * data_;
};

so we should not be worried about copying the few words around. (It might actually be even faster than dereferencing a non-local pointer.)

Still, I find this set of “member functions”

extern int stack_init(struct stack * this);
extern int stack_deinit(struct stack * this);
extern int stack_push(struct stack * this, char value);
extern int stack_pop(struct stack * this);
extern int stack_top(const struct stack * this);
extern int stack_size(const struct stack * this);

void
usage_example()
{
  struct stack mystack;
  if (stack_init(&mystack) < 0) { /* handle error */ }
  if (stack_push(&mystack, 'a') < 0) { /* handle error */ }
  assert(stack_top(&mystack) == 'a');
  assert(stack_size(&mystack) == 1);
  stack_deinit(&mystack);
}

much more consistent than this one:

extern int stack_init(struct stack * this);
extern int stack_deinit(struct stack * this);
extern int stack_push(struct stack * this, char value);
extern int stack_pop(struct stack * this);
/* Duckling asks: What is the difference between 'this' and 'self'? */
extern int stack_top(struct stack self);
extern int stack_size(struct stack self);

void
usage_example()
{
  struct stack mystack;
  if (stack_init(&mystack) < 0) { /* handle error */ }
  if (stack_push(&mystack, 'a') < 0) { /* handle error */ }
  /* Duckling asks: Are you sure you didn't forget the '&' here? */
  assert(stack_top(mystack) == 'a');
  assert(stack_size(mystack) == 1);
  stack_deinit(&mystack);
}

If possible, I think the fact that something is const should be the concern of the compiler and not force the programmer to use another calling convention.

It's irrelevant to the question but in case someone asks: The return value of the stack_top and stack_pop functions was meant to be the respective character or –1 on underflow. The other functions return 0 on success and a negative error code on error (eg out of memory).

Sign up to request clarification or add additional context in comments.

1 Comment

This is another case where const struct* wins : if there are a number of function already using struct* as parameter, it's logical to continue using struct*, otherwise the interface would no longer be consistent. I feel this is totally fair. Now I'm wondering if there are some cases where the other way round might be true : a set of functions where most of the time argument are directly passed, and where a sudden const struct* would feel "foreign" to the list of functions. I guess it's probably a less common use case, but the most important point here is : API consistency prevails.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.