Friday, July 17, 2020

Member Variable Initialization via With[...] Methods

There are a number of ways to initialize member variables in C++. One of the "recommended" methods is via a constructor, and arguments to it. While this is functional, I've personally been gravitating away from this paradigm in my own code of late, especially when there are multiple parameters involved, or multiple constructor overloads for different initialization scenarios. As the use-cases get more complex, it becomes harder to reason about the parameters passed, and more prone to maintenance-related errors.

The alternative method I'm gravitating toward is this:
class CFoo
{
    int nSomeValue = 0;
    
    inline auto& WithSomeValue( int nValue )
    {
        nSomeValue = nValue;
        return *this;
    }
};
Why is this "better" (for some subjective measurement of better)?
  • Members are named and self-explanatory, rather than positional
  • You do not have to initialize all members (only what you want to set)
  • Order is not important (aside from internal constraints)
Some downsides:
  • More verbose than positional arguments
  • No built-in language support for the paradigm*
  • Object must support "partial" initialized state, where some members may be not set yet
  • Efficiency is dependent on inlining, which the compiler may not do for some build types and usage scenarios
  • If not inline, may copy, which might be bad, especially if copy isn't "clean"
One of the significant benefits in my mind is the way this paradigm lends itself to code consistency and ease of refactoring. Consider this hypothetical use-case:
auto oObject = CSomeObject{}
    .WithOneVariable( 42 )
    .WithAnotherVariable( "blah" )
    .WithSomeClassAlso( oInstance )
    ;
What's nice about the above is that it's trivial to move around or comment out individual initialization elements, depending on the use-case. It's also easy to add overrides for custom types and/or other initialization paradigms; no messy constructor overloading, these are all just normal methods. Yes, the class coding is somewhat more verbose, but arguably the endpoint usage is much cleaner, which is a worthwhile trade-off in my mind.

* A bit about the lack of language support...

It would be really nice if C++ supported this concept as a language-level thing (as some other languages do, and/or are adding). Aside from the possibility that the call may not be inlined (which could cause the object to be copied), another downside is that it doesn't work cleanly with class hierarchy; that is, base class With-style methods return a base class reference, rather than the child class. Thus, you need to be aware of ordering of initialization in some cases, and this can be problematic (as the order is implicitly the reverse of typical constructor initialization order).

It would be really nice if C++ supported something like:
class CFoo
{
    int nSomeValue = 0;
    
    inline auto(*this) WithSomeValue( int nValue )
    {
        nSomeValue = nValue;
    }
};
In my hypothetical above, the language recognizes the extended "auto(*this)" syntax, and by-definition the method returns a reference to the "most specialized" type of the object upon which the method is called (as known at the point of instantiation, with ambiguity resulting in a compilation error). This would not only eliminate the need to specify the return itself (it being now implicit), but would also eliminate the possible issues with copying the object, and allow the compiler to make additional inferences for optimization. That's the dream, anyway.

Tuesday, July 7, 2020

std::optional<> is Useful

Consider this: How many times have you written code where a specific value for a type represents an implicit "empty" value? For example, a string value where an empty string was implicitly "no value"? A number where zero was the "default", "no value" case?

How many times has that bitten you later, where you needed to add secondary variables to indicate if the first value was valid or not? Alternatively, how many data structures have values which may or may not be populated by a caller, and the callee needs to rely on additional or implicit information to discern if the values are valid or not?

Enter std::optional<> (or previously boost::optional<>). This type can be used to express the "null" state for any type, without using secondary variables, implicit value meanings, or other mechanisms (eg: using pointers which might be null).

Usage is easy:
std::optional<std::string> osValue;
osValue.has_value(); // yields false
osValue.value_or( "something" ); // yields "something"
osValue = "else";
osValue.has_value(); // yields true
osValue.value_or( "something" ); // yields "else"

Here's the official reference, for reference: https://en.cppreference.com/w/cpp/utility/optional

Next time you think of a case where something might have a value or "nothing", considering using std::optional<>.