Search code examples
c++inheritancestructdesignated-initializer

Overload struct default values with Designated Initializers


Is it possible to achieve (at least something similar to) this? I need Designated Initializers for "named arguments" and/or possibility to skip setting of some params (not shown here). And still get this "cascade" default values.

Ideally I need set params of Derived (when instantiate) without knowledge of inheritance (because there should be lets say 5 level of inheritance and its user unfriendly have to know how many inheritances there is... ) Of course knowledge of params name and order is needed.

#include <iostream>

using namespace std;

struct Base
{
    string baseDefault = "Default";
    string label = "Base";
};

struct Derived : public Base
{
    // using Base::baseDefault; // do not help
    using Base::Base;
    string label = "Derived";
};

int main()
{
    Derived d1{.baseDefault="ChangedDefault", .label="NewDerived"};
    Derived d2{};
    Base b1{.label="NewBase"};
    Base b2{};

    cout    << "  d1: " << d1.label << d1.baseDefault
            << ", d2: " << d2.label << d2.baseDefault
            << ", b1: " << b1.label << b1.baseDefault
            << ", b2: " << b2.label << b2.baseDefault << endl;
    /* expect: d1: NewDerived ChangedDefault,
               d2: Derived Default,
               b1: NewBase Default,
               b2: Base Default
    */
    return 0;
}

I try to clarify it:

If no default values needed (or just one for each member), I can do this:

struct Base
{
    string withDefault = "baseDefault";
    string noDefault;
};

struct Derived : public Base
{
    string inDerived; /* no matter with/without default*/
};

int main()
{
    Derived d{{.noDefault="SomeSetting"}, .inDerived="NextSetting"};

    Base b{.nodefault="SomeSetting"};

    return 0;
}

But the problem is: I need to use different default value for /withDefault/ if I constructed /Derived/. So Something like this:

struct Derived : public Base
{
    string withDefault = "useThisAsDefaultHere";
    string inDerived; /* no matter with/without default*/
};


Solution

  • Consider the following, "naive", design:

    #include <iostream>
    
    struct Base {
      char const* Base_var1 = "Base_var1";
      char const* Base_var2;
    };
    
    struct Derived1 : public Base {
      char const* Base_var1 = "Derived1_var1";
      char const* derived1_var3 = "Derived1_var3";
    };
    
    struct Derived2 : public Derived1 {
      char const* derived2_var4 = "Derived2_var4";
      char const* derived2_var5 = "Derived2_var5";
    };
    

    Here we have five variables, ending on _var1, _var2, _var3, _var4 and _var5 respectively. Their prefix is the name of the class that they are first defined in. For example, Base defines Base_var1. Although Derived1 overrides the default, it still has the same name in Derived1 of course.

    We can thus state that _var1 has a default in Base that is overridden in Derived1. _var2 has no default, _var3-5 are introduced, with defaults, in Derived1 and Derived2 respectively.

    If now we want to construct an object of type Derived2 where we want to use all default values, except for _var3 and _var5 (and of course give _var2 a value) then we could attempt to do this as follows:

    int main()
    {
      Base b = { .Base_var2 = "main_var2" };
      Derived1 d1 = { b, .derived1_var3 = "main_var3" };
      Derived2 d2 = { d1, .derived2_var5 = "main_var5" };
    
      std::cout <<
        d2.Base_var1 << ", " <<
        d2.Base_var2 << ", " <<
        d2.derived1_var3 << ", " <<
        d2.derived2_var4 << ", " <<
        d2.derived2_var5 << std::endl;
    }
    

    This has several flaws. The most important one is that it isn't correct C++. When we try to compile this with clang++ we get:

    >clang++ -std=c++20 troep.cc 
    troep.cc:21:22: warning: mixture of designated and non-designated initializers in the same initializer list is a C99 extension [-Wc99-designator]
      Derived1 d1 = { b, .derived1_var3 = "main_var3" };
                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
    troep.cc:21:19: note: first non-designated initializer is here
      Derived1 d1 = { b, .derived1_var3 = "main_var3" };
                      ^
    troep.cc:22:23: warning: mixture of designated and non-designated initializers in the same initializer list is a C99 extension [-Wc99-designator]
      Derived2 d2 = { d1, .derived2_var5 = "main_var5" };
                          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
    troep.cc:22:19: note: first non-designated initializer is here
      Derived2 d2 = { d1, .derived2_var5 = "main_var5" };
                      ^~
    2 warnings generated.
    

    And the output of the program is:

    Derived1_var1, main_var2, main_var3, Derived2_var4, main_var5
    

    which is the desired result.

    With clang++ you only get a warning, but with g++ for example, it won't compile and you get the errors:

    >g++ -std=c++20 troep.cc
    troep.cc: In function ‘int main()’:
    troep.cc:21:22: error: either all initializer clauses should be designated or none of them should be
       21 |   Derived1 d1 = { b, .derived1_var3 = "main_var3" };
          |                      ^
    troep.cc:22:23: error: either all initializer clauses should be designated or none of them should be
       22 |   Derived2 d2 = { d1, .derived2_var5 = "main_var5" };
          |                       ^
    

    The second problem is that Derived1 isn't really overriding the value of Base::Base_var1 but instead is hiding it. If it would be passed to a function that takes a Base& then Base_var1 would have an "unexpected" value.

    The way I solved the first problem is by splitting the structs into *Ext (extension) structs that define the variables and (initial) defaults, and use classes without member variables for the inheritance. This way you don't run into the need to use a mixture of designated and non-designated:

    struct Base {
      char const* Base_var1 = "Base_var1";
      char const* Base_var2;
    };
    
    struct Derived1Ext {
      char const* derived1_var3 = "Derived1_var3";
    };
    
    struct Derived2Ext {
      char const* derived2_var4 = "Derived2_var4";
      char const* derived2_var5 = "Derived2_var5";
    };
    
    class Derived1 : public Base, public Derived1Ext {
    };
    
    class Derived2 : public Base, public Derived1Ext, public Derived2Ext {
    };
    

    Initialization of Derived2 then becomes:

      Derived2 d2 = {
        { .Base_var2 = "main_var2" },
        { .derived1_var3 = "main_var3" },
        { .derived2_var5 = "main_var5" }
      };
    

    which has extra braces, but is over all rather clean - and you still only have to specify values that you want to differ from the defaults.

    Of course, this still lacks the override of the default of Base_var1.

    But we can compile this without errors or warnings ;). The output is then

    Base_var1, main_var2, main_var3, Derived2_var4, main_var5
    

    The last step is to fix the first value here, without changing main() again.

    The only way I could think of is by using magic values... In the above case we only have char const* but in general this could become rather elaborate. Nevertheless here is something that does the trick:

    #include <iostream>
    #include <cassert>
    
    char const* const use_default = reinterpret_cast<char const*>(0x8);     // Magic value.
    
    struct BaseExt {
      char const* Base_var1 = use_default;
      char const* Base_var2 = use_default;
    };
    
    struct Base : BaseExt {
      void apply_defaults()
      {
        if (Base_var1 == use_default)
          Base_var1 = "Base_var1";
        // Always initialize Base_var2 yourself.
        assert(Base_var2 != use_default);
      }
    };
    
    struct Derived1Ext {
      char const* derived1_var3 = use_default;
    };
    
    struct Derived1 : BaseExt, Derived1Ext {
      void apply_defaults()
      {
        if (Base_var1 == use_default)
          Base_var1 = "Derived1_var1";      // Override default of Base!
        if (derived1_var3 == use_default)
          derived1_var3 = "Derived1_var3";
        BaseExt* self = this;
        static_cast<Base*>(self)->apply_defaults();
      }
    };
    
    struct Derived2Ext {
      char const* derived2_var4 = use_default;
      char const* derived2_var5 = use_default;
    };
    
    struct Derived2 : BaseExt, Derived1Ext, Derived2Ext {
      void apply_defaults()
      {
        if (derived2_var4 == use_default)
          derived2_var4 = "Derived2_var4";
        if (derived2_var5 == use_default)
          derived2_var5 = "Derived2_var5";
        BaseExt* self = this;
        static_cast<Derived1*>(self)->apply_defaults();
      }
    };
    
    int main()
    {
      Derived2 d2 = {
        { .Base_var2 = "main_var2"},
        { .derived1_var3 = "main_var3" },
        { .derived2_var5 = "main_var5" },
      };
      d2.apply_defaults();
    
      std::cout <<
        d2.Base_var1 << ", " <<
        d2.Base_var2 << ", " <<
        d2.derived1_var3 << ", " <<
        d2.derived2_var4 << ", " <<
        d2.derived2_var5 << std::endl;
    }
    

    EDIT: Improved version

    Instead of the use_default magic number you can use std::optional. And instead of having to call apply_defaults manually, you can add an other member (dummy) that does this for you. By adding the [[no_unique_address]] this addition is (probably) completely optimized away (the size of the struct does not increase (the use of std::optional DOES make the struct increase, of course)).

    #include <iostream>
    #include <optional>
    #include <cassert>
    
    //------------------------------------------------------------------------
    // Base
    struct BaseExt {
      std::optional<char const*> Base_var1 = std::optional<char const*>{};
      std::optional<char const*> Base_var2 = std::optional<char const*>{};
    };
    
    struct Base;
    struct BaseApply { BaseApply(Base&); };
    struct Base : BaseExt {
      void apply_defaults()
      {
        if (!Base_var1)
          Base_var1 = "Base_var1";
        // Always initialize Base_var2 yourself.
        assert(Base_var2);
      }
      [[no_unique_address]] BaseApply dummy{*this};
    };
    BaseApply::BaseApply(Base& base) { base.apply_defaults(); }
    
    //------------------------------------------------------------------------
    // Derived1
    struct Derived1Ext {
      std::optional<char const*> derived1_var3 = std::optional<char const*>{};
    };
    
    struct Derived1;
    struct Derived1Apply { Derived1Apply(Derived1&); };
    struct Derived1 : BaseExt, Derived1Ext {
      void apply_defaults()
      {
        if (!Base_var1)
          Base_var1 = "Derived1_var1";      // Override default of Base!
        if (!derived1_var3)
          derived1_var3 = "Derived1_var3";
        BaseExt* self = this;
        static_cast<Base*>(self)->apply_defaults();
      }
      [[no_unique_address]] Derived1Apply dummy{*this};
    };
    Derived1Apply::Derived1Apply(Derived1& derived1) { derived1.apply_defaults(); }
    
    //------------------------------------------------------------------------
    // Derived2
    struct Derived2Ext {
      std::optional<char const*> derived2_var4 = std::optional<char const*>{};
      std::optional<char const*> derived2_var5 = std::optional<char const*>{};
    };
    
    struct Derived2;
    struct Derived2Apply { Derived2Apply(Derived2&); };
    struct Derived2 : BaseExt, Derived1Ext, Derived2Ext {
      void apply_defaults()
      {
        if (!derived2_var4)
          derived2_var4 = "Derived2_var4";
        if (!derived2_var5)
          derived2_var5 = "Derived2_var5";
        BaseExt* self = this;
        static_cast<Derived1*>(self)->apply_defaults();
      }
      [[no_unique_address]] Derived2Apply dummy{*this};
    };
    Derived2Apply::Derived2Apply(Derived2& derived2) { derived2.apply_defaults(); }
    
    //------------------------------------------------------------------------
    int main()
    {
      Derived2 d2 = {
        { .Base_var2 = "main_var2"},
        { .derived1_var3 = "main_var3" },
        { .derived2_var5 = "main_var5" },
      };
    
      std::cout <<
        *d2.Base_var1 << ", " <<
        *d2.Base_var2 << ", " <<
        *d2.derived1_var3 << ", " <<
        *d2.derived2_var4 << ", " <<
        *d2.derived2_var5 << std::endl;
    }
    

    This prints as output:

    Derived1_var1, main_var2, main_var3, Derived2_var4, main_var5
    ^^^^^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^^^^^  ^^^^^^^^^
    Base_var1,     Base_var2  derived1_var3             derived2_var5
    default by                           derived2_var
    Derived1.                            default by
                                         Derived2.