Question

It recently occurred to me that more often than I'd like to admit, the fix for a "strange bug that only materializes occasionally" is to simply initialize the one member variable of a class I forgot to add to the initializer list.

To prevent wasting time on such bugs in the future, I've been toying with the idea to completely abandon built-in primitive types and replacing them with wrapper classes that act exactly like their primitive counterparts, except that they'll always be initialized.

I'm proficient enough in C++ to know that I can write classes that get very close to that goal. I.e. I'm confident I can write a MyInt class that behaves very much like a real int. But I also know C++ well enough to know that there's probably an arcane thing or two I'd be missing ;)

Has anyone done something like this before? Any pointers to relevant documentation, or list of pitfalls to watch out for? Is this even a good idea, or are there any downsides I'm not seeing?

Thanks!

EDIT: Thanks everyone for your comments, here's an update. I started playing with Jarod42's wrapper snippet and see if I could convert a small hobby project codebase. Not completely unexpectedly, it's quite a PITA, but would probably be doable. It does start feeling like a very big hammer for this problem though.

Compiler warnings are not a real option, since only a single one (-Weffc++) seems to find the problem and it only exists for gcc, i.e. this is not a safe, portable solution.

My conclusion so far is to start using C++11's in-class initialization for all primitive members as suggested by Praetorian below. I.e. something like

struct C {
    int x = 0;  // yay C++11!
};

.. and hope that after some time, ommitting such initialized feels as "naked" as declaring an unititialized variable in code within a function (which I've stopped doing a long time ago). This seems much less error-prone than trying to keep the initializer list(s) up-to-date, because it's right there with the declaration.

Était-ce utile?

La solution

C++11 makes it quite easy to avoid this pitfall by allowing in-class initialization of non-static data members. For example:

struct foo
{
  foo(int i) : i(i) {}    // here i will be set to argument value
  foo() {}                // here i will be zero

  int i = {};             // value (zero) initialize i
};

This feature is not restricted to trivial types either. So just start initializing data members as soon you declare them within the class definition.

If your compiler doesn't support this feature, try cranking the warning level up to see if it'll tell you about uninitialized data members. For instance, g++ has the -Weffc++ flag that will warn you about uninitialized data members (amongst other things).

Next thing to try would be a static analysis tool to catch these mistakes.

In conclusion, there are several things I'd try before going down the path of boxing every trivial data type.

Autres conseils

It's a lot easier to just turn on compiler warnings. Most decent compilers will warn you about using uninitialized variables - granted there are a few edge cases that compilers can miss.

Try better debugging.

You can switch on compiler warnings for uninitialized variables (see this SO question).

You can also use programs that do this and other checking of your code (static analysis), like cppcheck, which lists uninitialised variables.

Also try changing how you code. In C++ you have control over when memory is allocated, what constructors are used, and so on. If you are coding in a style where you construct objects with partial data, and then later populate other pieces, then you are likely to run into uninitialised variables a lot. But if you make sure that all the constructors construct a valid object in a valid state, and avoid having multiple points of truth (see Single Point Of Truth principle), then your errors are more likely to be caught by the compiler - you would have to pass an uninitialised variable as a value (which VC++ will warn you on), or have the wrong number or type of things in your constructor call (compile error), etc.

Might I suggest you pick out your most recent source of this kind of thing, complete with the chain of structures that got you there, and ask how you might restructure it better? There is a particularly disciplined style of coding in C++ which makes maximum use of the compiler and thus tips you off as early as possible. Really the bugs you create when using that style shouldn't be anything less than multi-threading issues, resource issues etc.

I worry that if you initialise everything just to prevent such errors, you'll miss out on learning that disciplined style which extends so much further than uninitialised variables.

Following may help:

template <typename T>
class My
{
public:
    constexpr My() : t() {}
    constexpr My(const T&t) : t(t) {}
    operator T& () { return t; }
    constexpr operator const T& () const { return t; }

    const T* operator& () const { return &t; }
    T* operator& () { return &t; }
private:
    T t;
};

Note sure if it is better to check that My<int> is used in place of each possible uninitialized int...

But note that you have to do special job for union anyway.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top