1

I'm created an opaque type and I have two options for its management.

The first looks like this:

type_t *a = type_init();
int err;

err = type_do(a, "foo", "bar", FLAGBIT1|FLAGBIT2, NULL);
if (!err) {
    type_print_result(a);
    ...
}

type_free(a);

The second is:

type_t *a;
int err;

err = type_do(&a, "foo", "bar", FLAGBIT1|FLAGBIT2, NULL);
if (!err) {
    type_print_result(a);
    ...
}

type_free(a);

type_do is the function that calculates the results we could work on. As you may underastand in the second case the initialization happens within type_do, that's why I have to pass the address of a. It is absolutely necessary to call type_do before doing anything else with type_t. Is there any reason to have a separate init function?

5 Answers 5

5

As a rule, I design the API so that the fewest possible things can go wrong. That means:

  • Only one init function so that there are no in-between states.
  • It must be safe to call the destruction function even if the init function fails, i.e. if (ptr == NULL) return; even if no other function has NULL checks or if NULL checks in other functions are fatal.
  • Ensure that the error can't be ignored, either by warn_unused_result (with appropriate -Werror settings) or make the error an argument (and return the result).
1
  • As an addition: If type_do must be called first (or immediately after type_init), then it really is doing initialisation. Sep 14, 2014 at 6:50
2

I prefer to view this situation as one requiring a Factory. The convention for a factory is that it always returns a valid object, or perhaps raises an exception if it really cannot. The code then looks like this.

type_t a = type_factory("foo", "bar", FLAGBIT1|FLAGBIT2, NULL);
if (!a.valid) {
    type_print_result(a);
    ...
}
type_free(a);

or

if (!type_is_ok(a)) { ... }

This idiom works better in OO languages, but is perfectly usable here too.


@glampert makes the excellent point that a factory guarantees a safe default in case there is a reason a new object cannot be created. It also makes it easier to mock the object for testing purposes.

2
  • 1
    This idiom is particularly useful if the resource you are trying to create should be turned into a "safe default" in case a new one can't be created.
    – glampert
    Sep 14, 2014 at 17:59
  • 1
    Excellent point. See edit.
    – david.pfx
    Sep 15, 2014 at 2:59
1

I see two reasons why a separate initializiation makes sense. First, there is the the "Single Level of Abstraction" principle, which helps to make code more readable and more evolvable. This means, if type_init and type_free are a pair of associated functions, it makes perfectly sense to call them at the same level whenever possible.

The second reason is that in case initialization fails, you might be able to catch that within the function above, so your code should probably look like this:

type_t *a = type_init();
int err;

if(!a) {
    // signal to the caller that initialization failed
    return;
}

err = type_do(a, "foo", "bar", FLAGBIT1|FLAGBIT2, NULL);
if (!err) {
    type_print_result(a);
        ...
}
else {
    // signal to the caller that type_do failed
}
type_free(a);

(nevertheless, type_free should still behave well when it gets a 0 passed, of course).

0

Given your description, the only reason I can see for a separate initialization function is to allow for the possibility that, sometime in the future, it will no longer be absolutely necessary to call type_do(type_t*, ...) before doing anything else with a type_t.

In any case, you should document that type_init() or type_do() returns an object that the caller should free by calling type_free() when it's no longer needed.

0

Yes, it makes sense to have a separate init function.

The first case is great: define a variable and initialize in 1 step. Easy to use paradigm.

In the 2nd case, if used, the code should be tolerant of initialization with 0 and/or NULL and acceptable to a subsequent type_free(). Rational: that what users will do as there is so much blanketed initialization with functions like memset(p,0,size) and calloc(), etc and count on that as acceptable coding. Why build a type_free() that doesn't cope well with 0?

type_t *a;
err = type_do(&a, "foo", "bar", FLAGBIT1|FLAGBIT2, NULL);
type_free(a);
...
type_t *b = 0;
type_free(b);

I would even advocate a tolerant type_free(0); in the first case too.

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.