3

Looking over Joint Strike Fighter Air Vehicle C++ Coding Standard, rule AV 73 states something on the lines: Default c++ constructors should be avoided if that means leaving object in a partially constructed place. This is obvious a good advice but things get complicated when you also disallow exceptions (as it does)

I wonder if they also try to give something else in return to make it practical. E.g. following example needs 2 faze construction if OUT_OF_MEMORY has to be handled (but the problem is more general of course)

class A {
    A() = default; // no way to report construction of B and C
    int Init(); // allocation failure of B/C can be reported only here
    private:
    B* b;
    C* c
}

One way to handle the problem is to have a A* Create() static member but in this case A has to free store allocated or keep an non-fully initialized state inside A and check it inside every member access, which seems not better at all than using a C style

int Initialize(A& out) // stack allocation
int Initialize(unique_ptr<A>& out) // free store allocation

EDIT I posted one possible approach bellow also. I took under consideration that dynamic memory allocation is necessary and the fact that any errors can happen while fully constructing an object. This is a scenario that is plausible for any driver development work.

3
  • 3
    Are you working on something that demands a jet-fighter level of reliability and is backed with a defense-contractor scale budget? Apr 4, 2015 at 19:40
  • @whatsisname it's not that strict, but for example working on kernel mode you usually can't/won't use exceptions
    – Ghita
    Apr 4, 2015 at 19:42
  • 1

3 Answers 3

7

Mandatory disclaimers

(1) Because people who have seen the code can't say anything about it, and people who can freely comment on it have never seen the actual code, all we can do here is to speculate, speculate, and to speculate. Therefore, here is not an answer, just a speculation.

(2) This is not the typical way I write C++ because most of the projects I work on allows exceptions, at least on a local basis (i.e. not crossing application boundaries), and the coding standard ensures that there are always appropriate exception catchers in the right place. This answer was written as if it is an interesting thought, not as a sharing of experience.


My opinion is that to avoid the issue of partially constructed object (or, "state"), one must first fundamentally change the way parameter (precondition) validation is performed.

The change is this: instead of validating and assigning parameters one-by-one, one must perform the complete validation of all parameters together, in a side-effect-free manner.

In addition to that change, the role of class constructor is also changed. Instead of handling both precondition validation and state initialization, it will "outsource" both to someone else; it will only retain the part of responsibilities that are "failproof" (not capable of failing).

For example, assigning a primitive value (e.g. integer) to a primitive variable is failproof, provided that the primitive variable has valid storage. Another example is the ownership-transfer of a pointer from one smart variable to another.

Some of the biggest advances of C++11 is that smart pointers (and many other things) are moving toward making at least some of the operations "failproof", by giving the option of isolating those "failable" (having the potential of failing) operations into separate methods.


Ultimately, however I must say, the "no exceptions" rule is sometimes impractical for at least some types of application development. How else would one prevent std::bad_alloc without exceptions? Should the system crash-and-burn?

Mission-critical systems prevent out-of-memory issues by ensuring system-wide determinism in memory usage. Everything is preallocated; objects are merely placement-newed on allocators. There is a maximum number of instances prescribed for each type of objects; attempt to exceed the maximum will either be rejected, or result in the yanking of another less-important, not-actively-in-use object.

This, may be why we keep hearing those memes about "this enemy tracking system is capable of simultaneously tracking 256 different objects." When a 257th object wants to be added to the system, one of the least-important object must go. Since none of us commenting here have seen any of the code, this is just a speculation.

4
  • I agree about std::bad_alloc. I think programming without exceptions can be a good thing in certain types of applications, but you always have to account for a tiny corps of "essential" exceptions: out of memory, divide by zero, null pointer. These are the exceptions that can still occur in languages and environments that do not have exception handling, such as C.
    – user22815
    Apr 6, 2015 at 1:06
  • @Snowman Sorry, but divide by zero and null pointer aren't unavoidable exceptions. Divide by zero always occur due to some reasons, and those will have some progenitors which is the place where the check should have been performed. This also applies to null pointers.
    – rwong
    Apr 6, 2015 at 1:49
  • If you always check everything before using it then yes. In practice this does not happen. My point is these are the three most common exceptions that can be generated by the execution environment whether you enable exceptions or not. You may use a third-party or even standard library that does not perform those checks. Code not under your control can cause them, so it makes sense to trap and handle them if bullet-proof software (e.g. runs a pacemaker or respirator) is your goal.
    – user22815
    Apr 6, 2015 at 2:44
  • 1
    It reminds me of one way of using C++ : as a C with methods, namespaces, and operator overloading.
    – rwong
    Apr 6, 2015 at 2:50
3

Default c++ constructors should be avoided if that means leaving object in a partially constructed place. This is obvious a good advice but things get complicated when you also disallow exceptions (as it does)

Not necessarily. Consider these rules:

  • a constructor should receive already validated arguments, and perform no operations outside of initializations/movement of values (constructor's responsibility is to initialize the object, not to compute values, then initialize the object - that would break SRP).

  • if an object's construction is non-trivial, computational part should be moved into a factory function

Example implementation:

class A {
public:
    A() = deleted; // no way to construct instance that completely initializes A

    // int Init(); // don't use two-phased construction; it imposes on client code

    A(B* b, C* c) : b_{ b }, c_{ c } {}

private:
    B* b_;
    C* c_;
};

enum result { success, error };

// return error code
result make_A(B_and_Q_store& store, A*& instance)
{
    B * b = store.buy_a_b();
    C * c = store.get_q().make_a_c();
    if(!C)              // ERROR HANDLING
        return error;   // ERROR HANDLING
    instance = new A{ b, c };
    return success;
}

Client code:

A* a = nullptr;
if(success == make_A(some_store, a)) // ERROR HANDLING
{
    // ...
}

This implementation offers convenience and good design for client code:

  • make_A performs failable computations;

  • constructor contains only trivial code;

  • law of demeter is respected: because an instance of A only uses B and C, it shouldn't know (or depend on) how B and C are constructed (because it is no longer responsible for instantiating them).

Similar to what @rwong states (when he mentions side effects and validation), parameter/argument values are validated as necessary, before you create the A instance (that is, when you get to creating the instance, you already know all parameter values are valid).

6
  • Thanks for the code example. For function make_A to return A by assigning to instance, the argument instance might need to be either a A ** (requires a dereference to assign), or A*& (to make the pointer assignment visible to its caller).
    – rwong
    Apr 7, 2015 at 14:25
  • @utnapistim Only thing is that if class A has lots of dependencies it's no longer practical to say A is not responsible for creating it's dependencies . In real world I am not interested on how class A handles it's implementation details
    – Ghita
    Apr 8, 2015 at 19:51
  • I posted a response with another solution
    – Ghita
    Apr 8, 2015 at 19:52
  • @Ghita, if class A has a lot of dependencies, it is the responsibility of the factory function to handle those. That's the whole point of the law of Demeter. Once you put those in the constructor, the entire class becomes a monolith that cannot be tested, reused or extended properly.
    – utnapistim
    Apr 8, 2015 at 21:20
  • Not in a clasical sense dependencies. If there is not a need to see them replaceble, e.g. they not implement an inteface why should i care. Besides that is not really my point I dont like out params.
    – Ghita
    Apr 9, 2015 at 3:56
1

One way to handle these is to have factory functions:

expected<Missile> Missile::Create(Params...)
expected<unique_ptr<Missile>> Missile::Create(Params...)

class Missile {
  private:
   Missile() = default;
   int Initialize(int power) // second phase constructor {
     // other operations (besides out_of_memory) that may fail
     // can be reported from this function
     expected<PowerSupply> expectedPower = PowerSupply::Create(power);
     if (!expectedPower.valid()) {
       return expectedPower.get_error();
     }
     this->power = std::move(expectedPower); // from now on power.valid() true
     return SUCCESS;

  public:
  static expected<Missile> Missile::Create(int p) {
    // 1. call default constructor first, this is automatic
    // storage so no fail
    // 2. Initialize called. 
    // If failure - report expected<Missile>::from_error(Initialize_STATUS);
    Missile m;
    auto status = m.Initialize(p);
    if (status != 0) return expected<Missile>::from_error(status);
    return Missile; // implicit conversion from T to expected<T>
  }
  static expected<unique_ptr<Missile>> Missile::Create(int p) {
    // 1. use operator new to construct - may fail -> expected::from_error(OUT_OF_MEMMORY)
    // 2. call Initialize and possibly report second phase errors
  }

  private:
  int power;
  expected<PowerSupply> power;
}

This is cleaner than using out parameters and a complex object is able (using 2 phases) to build it's dependencies, be it as automatic variable or free store allocated.

Class expected default constructor only allocates automatic storage for T. One can create an initialized expected instance by assigning a T instance to expected<T>

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.