9

Pre C++11 you had no non-static member initializion nor did you have construction delegation, so people often used private helper functions to help with initializtion to reduce code replication.

Is this good code in 2018?

class A  {
  int a1 = 0;
  double a2 = 0.0;
  string a3 = "";
  unique_ptr<DatabaseHandle> upDBHandle;

  void init(){
      upDBHandle = open_database(a1, a2, a3);
  }

public:
    A() { init(); }
    explicit A(int i):a1(i) {  init(); }
    explicit A(double d):a2(d) {  init(); }
    explicit A(std::string s):a3(std::move(s)) {  init(); } 
    A(int i, double d, std::string s) : a1(i), a2(d), a3(std::move(s)) { init(); }
};

How can this code be improved?

code
  • 1,056
  • 10
  • 22
  • 9
    No, it's not. And even prior to C++11 `init` could just as easily have returned a fully constructed handle to initialize the member handle with. – StoryTeller - Unslander Monica Jan 19 '18 at 21:33
  • 7
    That's horrible both pre and post C++11. – Jesper Juhl Jan 19 '18 at 21:35
  • 4
    I believe this question would be better asked at [codereview](https://codereview.stackexchange.com). – zett42 Jan 19 '18 at 21:47
  • 4
    @StoryTeller I'm not sure if it's nearly as horrible as you're making out. The worst thing with init function is when they are public and you have two phase initialization. That's not the case here. What does a better solution look like if there is more than just one member variable that you want to initialize based on the values of a1, a2, and a3? C++ doesn't have good ways of handling multiple defaults, and the awkward solution reflects that. – Nir Friedman Jan 19 '18 at 21:47
  • 1
    This might be a good candidate for the [Named Parameter Idiom](https://stackoverflow.com/a/2700976/10077). – Fred Larson Jan 19 '18 at 21:48
  • @NirFriedman - Several, free, `init_1` to `init_n` functions that are preferably not even in `A`'s header file. Any excuse being made for this, is just that, an excuse. – StoryTeller - Unslander Monica Jan 19 '18 at 21:51
  • @NirFriedman Delegating constructors for this example. If a delegating constructor is not appropriate then I would probably consider a factory/builder. – 0x5453 Jan 19 '18 at 21:52
  • 1
    @StoryTeller Uhm what? I have no idea what you mean. Maybe you should answer and then we'll see? If you were saying free factory functions, then maybe... – Nir Friedman Jan 19 '18 at 21:53
  • 1
    @0x5453 A delegating constructor does not solve this nicely because you would have to repeat default values. The same is true with factory functions; both the factory function taking just a string, and the one taking just double, need access to the default value for int. – Nir Friedman Jan 19 '18 at 22:04
  • Using delegating constructor example. https://godbolt.org/g/JvVnLw – balki Jan 20 '18 at 00:16
  • @balki Right, you just didn't write the same constructors that OP did. If you did write a constructor just taking double and just taking string, you would run afoul of what I wrote above: repeating the default value for int. – Nir Friedman Jan 20 '18 at 15:15
  • string a3 = ""; is useless initialization as string will be empty by it's default ctor. No need in this line. – Spock77 Jan 24 '18 at 21:10

4 Answers4

5

In my opinion, your code is fine. I try to avoid relying on subtle effects such as the member initialization order in constructor initialization lists. It violates DRY - you need to repeatedly use the same order: In the class body when declaring the members, and in the constructor initialization list. As soon as time goes by and the class becomes bigger, and you move the constructors into the .cpp file, things start getting more confusing. Therefore, I put things that require access to other members into init functions.

If the member is const, you can't do this. But then again, as the class author you can decide which member is const and which is not. Note that this is not to be confused with the anti-pattern of "construct, then init", because here the init happens within the constructor, and this is invisible to class users.

If you still mislike the use of init, I would advice against putting the call into the constructor initialization list. Perhaps for me an acceptable midway is to put it into the in-class initializer, and remove all calls from the constructors.

class A  {
  int a1 = 0;
  double a2 = 0.0;
  string a3 = "";
  unique_ptr<DatabaseHandle> upDBHandle = open_database(a1, a2, a3);

  // ...
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • Difference of opinions we have on this aside, your last suggestion is hopelessly broken. Ironically, it breaks because of those subtleties you don't like to depend upon. – StoryTeller - Unslander Monica Jan 19 '18 at 23:53
  • @StoryTeller there is no subtlety going on here. The DRY rule is not violated. You can always move `upDBHandle` down so it is the last member, and need not rely on its position as last member anywhere else. – Johannes Schaub - litb Jan 19 '18 at 23:57
  • Not my point. You depend on the members being initialized in a specific order. Member initializer lists or not, it still depends on it. Or is that not what you meant by "subtle" effects? The order of members in a mem-initializer list is non-important. – StoryTeller - Unslander Monica Jan 19 '18 at 23:59
  • @StoryTeller the "subtlety" that I did refer to, was that the order in the constructor initialization list is ignored, in favor of using the declaration order, which is at a different place in the program. I can't speak for others, but for me, my code snippet is similar in subtlety as `int main() { int a = 1; return a + 2; }`, albeit a bit more subtle. Therefore it's merely an "acceptable midway" for me, and not my favourite solution. – Johannes Schaub - litb Jan 20 '18 at 00:03
  • I suggest you sharpen that point then. "Two places" isn't quite specific enough. I took it to mean the different c'tors. – StoryTeller - Unslander Monica Jan 20 '18 at 00:05
  • @StoryTeller I did elaborate on it a bit more. – Johannes Schaub - litb Jan 20 '18 at 00:10
  • 2
    +1 automatically for telling OP his code is fine; I feel bad for OP that the most upvoted things are telling him that his code is horrible, even though giving a better solution in C++ is not easy! May also be worth mentioning the -Wreorder warning that most (all?) major compilers support; for someone who's not aware of it this flag is an enormous help at avoiding some of the subtleties you mentioned. – Nir Friedman Jan 20 '18 at 15:16
  • @Johannes Thank you for your post. It was a tough decision whom to award the points to. – code Jan 20 '18 at 18:02
3

C++ just isn't very good at dealing with multiple defaults. So doing this nicely is not going to be easy. There are different things you can do, but all of them have different trade-offs (e.g. scattering defaults around).

IMHO, the nicest solution that can be arrived at here, is one that isn't legal C++ (yet), but is a highly supported extension: designated initializers.

class A  {
  struct Params {
      int a1 = 0;
      double a2 = 0.0;
      string a3 = "";
  };
  Params p;
  unique_ptr<DatabaseHandle> upDBHandle;

public:
    explicit A(Params p_arg) 
      : p(std::move(p_arg))
      , upDBHandle(open_database(p.a1, p.a2, p.a3) { }
};

A a({});  // uses all defaults
A a2({.a2 = 0.5});  // specifies a2 but leaves a1 and a3 at default
A a3({.a1 = 2, .a2=3.5, .a3 = "hello"});  //  specify all
Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
  • 1
    Probably because it doesn't address the core issue of the question, `upDBHandle`. – StoryTeller - Unslander Monica Jan 19 '18 at 22:24
  • @StoryTeller The entire problem is caused by having multiple constructors, having multiple constructors is caused by wanting different combinations of defaulting... This code solves exactly the use case of the original problem. – Nir Friedman Jan 19 '18 at 22:25
  • @StoryTeller Not sure what to take away from that. It avoids duplication of both member initialization and the default values, and lets the user construct an `A` with any combination of defaults and overrides they want. Wasn't that the goal of the original problem? If the user did not care about defaults, they could just define one constructor taking all three params and be done with it. Or if they only wanted to support left-to-right overrides, used standard C++ default parameters. – Nir Friedman Jan 19 '18 at 22:28
  • About commenting and downvoting: apparently there is a new policy, as I got the message: "Comments cannot contain that content. Don't comment on your downvote. If you think this post can be improved, please offer specific guidance. See https://stackoverflow.com/help/privileges/comment ." (BTW, my reason to downvote: a question about how to do things in C++11/C++14/C++17 should not be answered using something not in those versions - IMHO that comment is constructive as it indicates how to improve the answer, but the AI seems to think otherwise) – Sjoerd Jan 19 '18 at 22:48
  • @Sjoerd I agree your comment is constructive. But I definitely disagree with it. Since gcc, clang, and icc all support designated initializers to varying degrees, and OP has not said what their compiler is. There is a perfectly good chance that someone could have a similar problem, happen to be using clang/gcc, and be able to use this to solve their problem. That's what answers should be judged on: how well do they help someone who has the same problem as the question? – Nir Friedman Jan 19 '18 at 23:11
3

You could use the fact that, if a member is not initialized in a constructor's member initialization list, the default member initializer is executed. Moreover each member initialization is a full expression and the member initializations are always executed in the order of their declarations inside the class:

class A  {
  int a1 = 0;
  double a2 = 0.0;
  string a3 = "";
  unique_ptr<DatabaseHandle> upDBHandle = open_database(a1,a2,a3);
  //a1,a2 and a3 initializations are sequenced before
  //upDBHandle initialization.
public:
  //all these constructors will implicitly invoke upDBHandle's default initializer 
  //after a1,a2 and a3 inialization has completed.
  A() { }
  explicit A(int i):a1(i) {  }
  explicit A(double d):a2(d) {  }
  A(int i, double d, std::string s) : a1(i), a2(d), a3(std::move(s)) { }
};
Oliv
  • 17,610
  • 1
  • 29
  • 72
-4

Well, here's the thing, there is actually no need at all for init in your example. The language rules already dictate default member initializer go first, then what ever you do in the member initializer list. The members are initialized in declaration order. So you could really just define each c'tor as

A() upDBHandle(open_database(a1, a2, a3)) { }
explicit A(int i):a1(i), upDBHandle(open_database(a1, a2, a3)) {}
explicit A(double d):a2(d), upDBHandle(open_database(a1, a2, a3)) {}
explicit A(std::string s):a3(std::move(s)), upDBHandle(open_database(a1, a2, a3)) {} 
A(int i, double d, std::string s) : a1(i), a2(d), a3(std::move(s)), upDBHandle(open_database(a1, a2, a3)) {}

And that's it. Some may say it's not good, since changing the class declaration can make it break. But compilers are pretty good at diagnosing this. And I belong to the school of thought which says a programmer should know what they are doing, and not just build code by happy coincidence.

You can even do that pre-C++11. Sure, you don't have default member initializers, and would have to repeat the default values for each member (possibly name them somehow first, to avoid magic numbers), but that isn't related to the issue of initializing the member that depends on their initial value.

Advantages over init?

  • Support for const members, however rare they may come.
  • Support for objects with no default state. Those cannot be default initialized and wait until init is called.
  • No need to add superfluous functions to your class, that are unrelated to its function.

Now, if the initialization code for the member is not trivial, you can still put it in a function. A free function, that is hopefully static to your classes' translation unit. Like so:

static std::unique_ptr<DatabaseHandle> init_handle(int a1, double a2, std::string const& a3) {
  // do other stuff that warrant a function block
  return open_database(a1, a2, a3);
}

A::A() upDBHandle(init_handle(a1, a2, a3)) { init(); }
A::A(int i):a1(i), upDBHandle(init_handle(a1, a2, a3)) {}
A::A(double d):a2(d), upDBHandle(init_handle(a1, a2, a3)) {}
A::A(std::string s):a3(std::move(s)), upDBHandle(init_handle(a1, a2, a3)) {} 
A::A(int i, double d, std::string s) : a1(i), a2(d), a3(std::move(s)), upDBHandle(init_handle(a1, a2, a3)) {}

That also means that you can have several of those for different members. So now the concern of initializing the members is more spread out.

Removing the need for the many c'tors can actually be done with something that Fred Larson suggested in his comment to your post.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 1
    Great, and when you have multiple member variables that need to be initialized based on `a1`, `a2`, `a3`, what then? This solution scales worse than the original answer, and what exactly is the benefit? – Nir Friedman Jan 19 '18 at 22:22
  • @NirFriedman - Then what? `upDBHandle2(open_database(a1, a2, a3))`. Same as before. – StoryTeller - Unslander Monica Jan 19 '18 at 22:23
  • 1
    Great, so every time you add or remove a member variable, you also have to add/remove it in 5 (you left in an `init` by accident in the default constructor) initializer lists. In other words, your answer has considerable code duplication. Which is what the question is asking how best to avoid. – Nir Friedman Jan 19 '18 at 22:24
  • @NirFriedman - Oh, and how would *your* answer initialize `upDBHandle2`? Or is `upDBHandle2(open_database(p.a1, p.a2, p.a3)` somehow *less* code duplication? – StoryTeller - Unslander Monica Jan 19 '18 at 22:25
  • 2
    My answer only has one initializer list, not 5, because I only have one constructor, not 5, because I handle the defaults in a different way. So yes, there is "somehow" less code duplication. – Nir Friedman Jan 19 '18 at 22:26
  • @NirFriedman If you have multiple members all depending on the same parameters, maybe they should be grouped in a different class. And guess what! The constructor of that class is exactly the same as the init() function. So seeing this pattern in code screams "extract class refactoring" to me. – Sjoerd Jan 19 '18 at 22:52
  • @Sjoerd That's a valid answer, with downsides of its own. Again, I'm not sure what is really concrete better about than an init function, seems hard to believe it's easier to read. But in any case I'd suggest posting it. But that's not this answer; this answer doesn't answer the question, because the question says: how can I improve this technique to remove duplication? and the answer is simply to duplicate, with no argument that duplication is better than the original suggested technique. – Nir Friedman Jan 19 '18 at 23:05
  • 1
    @NirFriedman - Good luck "not duplicating" with objects that have no default state (in particular, and even with `init`). Oh, yeah, I suppose the classes can be twisted into having a default state, but now that's a design smell. – StoryTeller - Unslander Monica Jan 19 '18 at 23:08
  • @StoryTeller Almost every class in the standard library has a "default state" by virtue of having a default constructor. There is nothing wrong with the problem requirements, and in many languages (like python) you could solve this problem without any duplication, without hesitating, and people do it all the time. And in C++20, my solution will be conforming C++ that also solves the problem exactly to requirements, without any needless duplication. The fact that your answer doesn't answer the question, doesn't mean it's impossible. – Nir Friedman Jan 19 '18 at 23:14
  • 1
    @NirFriedman - Some of us look beyond the standard library, and do not like forcing a default state where it doesn't belong, just to appease some misguided sense of cleanliness. I most certainly do answer the question, that was asked, not the one you interpreted out of it. – StoryTeller - Unslander Monica Jan 19 '18 at 23:15
  • @StoryTeller The question asks specifically about avoiding duplication. We've already established that every member added to your class will involve adding the exact same line of code in 5 different places. How does that avoid duplication? – Nir Friedman Jan 19 '18 at 23:17
  • 2
    @NirFriedman - No, the question asks if this is good code in 2018. And more specifically, helper `init` functions have been considered bad since the C++ faq was first published. Regardless of their accessibility. – StoryTeller - Unslander Monica Jan 19 '18 at 23:17
  • @StoryTeller Okay, if you want to be literal: it still doesn't answer the question, because you are saying their code is bad, and not showing your solution is better, because you do not even acknowledge the duplication it introduces, much less weighing it against your "advantages" (one of which is code smell, another of which is rather odd). – Nir Friedman Jan 19 '18 at 23:20
  • This would be better if `init` function returned the unique pointer and then only `init` needs to be changed if things need to be changed later on. – NathanOliver Jan 19 '18 at 23:20
  • @NathanOliver I agree, but again, this is not extensible to even two members. – Nir Friedman Jan 19 '18 at 23:21
  • @NathanOliver - Fair point. I might as well add my original thoughts from the comments to the OP. – StoryTeller - Unslander Monica Jan 19 '18 at 23:22
  • @nirfriedman there is that. It would be a pain to have one helper for each member function. – NathanOliver Jan 19 '18 at 23:23
  • 2
    @StoryTeller The primary reason, by a mile, that init functions are bad, is and has always been that they encourage two phase initialization. Private init functions can be a useful technique; factory function returning optional, calling private default constructor + init function is known as the best way to handle failure when exceptions must be avoided. Ironically, the C++ FAQ *specifically* recommends private init functions to share code, prior to delegated constructors being available: https://isocpp.org/wiki/faq/ctors. So what you are saying is flat out wrong. – Nir Friedman Jan 19 '18 at 23:27
  • @NirFriedman - Were you trying to link a specific item, or am I supposed to search for the one you have in mind? – StoryTeller - Unslander Monica Jan 19 '18 at 23:29
  • @StoryTeller You can C-f "share". To be honest, the entire thinking here is pretty weird. Someone has some reasonable requirements, you insist on initializer lists over init functions although they are advantageous only in a few edge cases, even though they commit the much larger sin of duplicating code... Maybe it's just time to admit that intializer lists, while natural to the language, and preferred, have some major shortcomings, and it's reasonable to find workarounds to that, even if those workarounds are also not ideal? – Nir Friedman Jan 19 '18 at 23:31
  • 2
    @NirFriedman - No, I think the reasonable thing is to stop indulging this discussion. I won't be C-f-ing to prove *your* argument. Good night. – StoryTeller - Unslander Monica Jan 19 '18 at 23:33
  • 1
    @StoryTeller And when even that excuse is gone? https://isocpp.org/wiki/faq/ctors#init-methods – Nir Friedman Jan 19 '18 at 23:40
  • @NirFriedman - I do believe I have 2 out of 3 bullets to cover that. "Sometimes" is not "you should reach for it". If your own answer is "useful" for other that come here after 2020, then mine is most certainly useful to those with const members or non-default classes. And now, really, good night. – StoryTeller - Unslander Monica Jan 19 '18 at 23:43
  • "Support for objects with no default state. Those cannot be default initialized and wait until init is called.". I think this is off the line. In this case, the `init` is called within the default constructor, so there is no issue. – Johannes Schaub - litb Jan 19 '18 at 23:52
  • @JohannesSchaub-litb - There is no `const` object either. I was trying to make a broader point – StoryTeller - Unslander Monica Jan 20 '18 at 00:09