3

Lets say I have a class A that has a member of type int. I have a class B which is a subclass of A. B is meant to initialize the members to some state and has no other purpose.

#include <string>
#include <iostream>
struct A {
    int someInt;
    A() : someInt(33){}
};

struct B : public A {
    B() {
        someInt = 4;
    }
};
int main() {

    A a = A();
    A b = B();
    std::cout<< a.someInt << std::endl;
    std::cout << b.someInt << std::endl;
}

Notice how I use A b = B() where object slicing should occur. However since B doesn't add anything to A, is it a valid alternative to using A with different constructor parameters (or any other form of creating instances of A)?

Edit: The background is that I have a class that has some complex setup. Putting the initialization into separate child class is way easier than lets say write a constructor, factory or builder.

Raildex
  • 3,406
  • 1
  • 18
  • 42
  • 1
    Why not just add extra constructors to `A`? – m88 May 11 '21 at 14:36
  • 3
    *"Putting the initialization into separate child class is way easier than lets say write a constructor, factory or builder."* I don't see how it would be easier... – Jarod42 May 11 '21 at 14:37
  • 2
    Biggest problem with this practice IMHO that you do not initialize members there, you assign value to them. – Slava May 11 '21 at 14:41
  • @Jarod42 because you can compose very similar object instances but with lets say one different member this way. B subclasses from A and C subclasses from B which means C is exactly the same as B but only some desired internals are different. Having a constructor/factory/Builder means I need to provide the desired parameters for the construction somewhere (with subclasses the parameters are already the type/constructor of the subclass) – Raildex May 11 '21 at 14:41
  • what is `A` actually? And do you really want `B` to be a different distinct type? – 463035818_is_not_an_ai May 11 '21 at 14:43
  • Ho do you prevent somebody or yourself to add members to class B? – Slava May 11 '21 at 14:44
  • If you need different ways to initialize the field, then use as many constructors as you need. – Jean-Baptiste Yunès May 11 '21 at 14:44
  • 2
    The question you should be asking is does it make logical sense that B inherits A? https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse Implementation convenience should not be the main factor for designing your hierarchy. –  May 11 '21 at 14:45
  • `A makeA_versionC() { A a /* = makeA_versionB()*/; a.someInt = 4; return a; }` is as simple than inheritance (and more natural). (if field is private, `friend` function or static function of the class) – Jarod42 May 11 '21 at 14:45
  • Maker functions returning named objects require the object to be copyable or movable, which isn't always possible, making subclassing the only way I can see to do that in such cases. Hopefully we'll soon get guaranteed copy elision for NRVO and then can use maker functions though. – underscore_d May 11 '21 at 15:15
  • `A b = B();` is [object slicing](https://stackoverflow.com/questions/274626/what-is-object-slicing). It doesn't do much harm in your example, but in general its better avoided (because usually it does harm) – 463035818_is_not_an_ai May 11 '21 at 15:17
  • 1
    As your class B is nothing else as A, only initializing with another value, your solution looks obscure! You should use creator methods in that very simple case. BTW: You class B constructor did not initialize your member but assign after construction, which is typically not what you want, even if it is typically optimized away. But it looks ugly! – Klaus May 11 '21 at 15:27

3 Answers3

5

More common and less error prone method is to define static methods to create your class instances:

struct A {
    static A makeA();
    static A makeB();
};

int main()
{
    auto a = A::makeA();
    auto b = A::makeB();
}

and to prevent even more errors you may want to prohibit creation of instances of A directly, rather than set of static methods by making A ctor private.

Slava
  • 43,454
  • 1
  • 47
  • 90
3

To answer your question as directly as possible, what you're doing is certainly "valid" in the sense that it will compile and run and produce a correct result. It's probably not the most common way to accomplish this though.

My recommendation for a more idiomatic approach would be to use a common base class and templatize all of your derived versions. Basically make the compiler do the work of setting the integer value you want.

struct ABase {
    int someInt;
    ABase() = delete;
    ABase(int initVal) : someInt(initVal) { }
};

template<int T>
struct A : public ABase {
    A() : ABase(T) {}
};

int main() {
    ABase a = A<33>();
    ABase b = A<4>();
    std::cout << a.someInt << std::endl;
    std::cout << b.someInt << std::endl;
}

This is probably less code overall (especially if you have more than just two versions of this class), and much more flexible.

Jon Reeves
  • 2,426
  • 3
  • 14
  • 1
    Still inheritance here is quite artificial. You can make `A` a template function that returns instance of `ABase` and it will work quite the same (you do not even have to change code in `main()`). And note it would be even better - try to use `auto` instead of explicitly give `a` and `b` types, you may get some problems. – Slava May 11 '21 at 16:05
  • It's true, in this case inheritance is artificial. But questions on SO are usually asked with only the bare minimum of info, and we don't actually know how the OP is going to use this in practice. For example they may want the base class to have a vtable of some kind with truly polymorphic behavior. In this case some kind of inheritance is required. I usually try to give the benefit of the doubt for the asker's intent in questions like this. – Jon Reeves May 11 '21 at 16:44
  • "For example they may want the base class to have a vtable of some kind" in this case this solution will not work at all due to slicing. – Slava May 11 '21 at 17:35
  • It's definitely very good that you point that out, but that's a problem you'll only have if you literally only use the objects exactly as they're written in this incarnation of `main()`. I highly doubt that the `main()` function written here is anything like the true `main()` being used in practice. The problem as always is that we don't know exactly what the "real" code looks like, and how these classes are being used. We'd need the author to weigh in on that to know what's most appropriate. – Jon Reeves May 11 '21 at 18:11
0

As a better practice,
Use another constructor in class A and this constructor will take parameters OR Use a separate function in class A to do what you want.

And regarding "Putting the initialization into separate child class is way easier than lets say write a constructor, factory or builder" doesn't make sense in this situation, so it's not easier even the code is complex as you said.

Abanoub Asaad
  • 510
  • 4
  • 14