1

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*/
};

  • 1
    You might be interested by [named-arguments](https://www.fluentcpp.com/2018/12/14/named-arguments-cpp/). – Jarod42 Mar 12 '21 at 12:01
  • @Jarod42 cool, but not sure whether it helps (how to use it) with skip-able default values (which is the main problem I am solving) – Jaroslav Holeček Mar 12 '21 at 12:21
  • My answer for [how-to-generate-all-the-permutations-of-function-overloads](https://stackoverflow.com/a/30561530/2684539) might interest you too. – Jarod42 Mar 12 '21 at 13:01
  • I have to say I don't really understand what you want. But I suggest taking at a look at the named parameter idiom. http://www.cs.technion.ac.il/users/yechiel/c++-faq/named-parameter-idiom.html – Marius Bancila Mar 12 '21 at 13:06
  • @MariusBancila Looks quite well -> only problem is, if parameters are some large (hard to create) objects -> in this solution it first create some default and then change it I need use provided value if provided directly without creating default. If I understand it well. – Jaroslav Holeček Mar 12 '21 at 15:06
  • I gave this a serious try, but my finding is that it is not possible. You can only initialize a POD this way, so it can not have a constructor, or virtual function. And doing it in two steps: first initialize the defaults and then override with designated initializers also can't work, because the latter will always overwrite the values that an object already has in that case. Possibly you could do it using magic values though: use the same magic value of "use default" and then overwrite those in a second step with the real values. – Carlo Wood Jul 13 '21 at 14:17

1 Answers1

1

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.
Carlo Wood
  • 5,648
  • 2
  • 35
  • 47
  • Thank you for your investigation. As I understand it: use_default is some value, that is surely identified as "not from user" and apply_defaults() iterativly check whether value is already fill in by user or by derived class. Basicly it revert order of constructor if we see it from OOP perspective - so struct as class and apply_default() as constructor. Its realy nice. My problem is, that I need to use this struct as helper for achive named-argument - so use this struct directly (without creating d2 from expample) in other class constructor - so I (probably) can not call apply_defaults() – Jaroslav Holeček Jul 15 '21 at 08:33
  • Yes to your first sentences, till "My problem is". I do not understand what you mean after that, except that if you can't call a function after constructing an object with designated initializers then I am pretty sure that you can't achieve what you want. – Carlo Wood Jul 15 '21 at 23:02
  • Overall result should be: I have some inheritance structure of classes - In constructor of these objects I want to achieve described behavior (overloaded default value) and have named-arguments (because there is a lot of arguments). Solution of named-arguments is based on structs from this question - I use this struct directly in argument of constructor of object ( new Object({.arg1="one", .arg2="two"}) ). – Jaroslav Holeček Jul 17 '21 at 07:50
  • @JaroslavHoleček I added an improved version that no longer requires you to call `apply_defaults()`! So this should be usable for your use-case, no? Also switched to `std::optional` instead of using magic values. – Carlo Wood Jul 17 '21 at 17:47