1

My friend and I are going over inheritance and can't figure out what we're doing incorrectly.

We have the inheritance structure A->B->D and C->D.

In this shorter example, we are wondering

1 - Why won't this code work without the lines marked A and B?

#include <iostream>
#include <string>

class A {
protected:
    std::string m_name;
public:
    std::string name()                  { return m_name; }
    A* name (std::string str)           { m_name = str; return this; }
};

class B : public A {
protected:
    int m_num;
public:
    std::string name()                  { return m_name; } // <--- A
    int num()                           { return m_num; }
    B* name (std::string str)           { m_name = str; return this; } // <--- B
    B* num (int x)                      { m_num = x; return this; }
};

int main() {
    A a;
    B b;
    a.name("alice");
    b.name("bob")->num(10);
    std::cout << a.name() << " " << b.name() << " " << b.num() << std::endl;
    return 0;
}

In this longer example, we're wondering why the following 5 errors are popping up, because it's preventing us from progressing to our goal.

2 - How can we inherit what we need here?

#include <iostream>
#include <string>

class A {
protected:
    std::string m_name;
public:
    A()                                 = default;
    A(std::string str) : m_name(str)    { }
    A(const A& a)                       = default;
    std::string name()                  { return m_name; }
    A* name (std::string str)           { m_name = str; return this; }
};

class B : public A {
protected:
    int m_num;
public:
    B()                                 = default;
    B(std::string str, int x)
        : m_name(str)   // 1. class 'B' does not have any field named 'm_name'
        , m_num(x)                      { }
    B(const B& b)                       = default;
    std::string name()                  { return m_name; } // <--- A
    int num()                           { return m_num; }
    B* name (std::string str)           { m_name = str; return this; } // <--- B
    B* num (int x)                      { m_num = x; return this; }
};

class C {
protected:
    std::string m_name;                 //same as in A
    unsigned long m_bigNumber;
public:
    C()                                 = default;
    C(std::string str, unsigned long big) : m_name(str), m_bigNumber(big) { }
    C(const C& c)                       = default;
    std::string name()                  { return m_name; }
    unsigned long bigNumber()           { return m_bigNumber; }
    C* name (std::string str)           { m_name = str; return this; }
    C* bigNumber (unsigned long big)    { m_bigNumber = big; return this; }
};

class D : public B , public C {
private:
    std::string m_thing;
public:
    D()                                 = default;
    D(std::string str1, std::string str2, int x, unsigned long big)
        : m_thing(str1)
        , m_name(str2)  // 2. request for member 'm_name' is ambiguous
        , m_num(x)      // 3. class 'D' does not have any field named 'm_num'
        , m_bigNumber(big)              { }
    D(const D& d)                       = default;
    std::string thing()                 { return m_thing; }
    D* thing (std::string str)          { m_thing = str; }
};

int main() {
    A a("alpha");
    B b("bravo", 100);
    C c("cookie", 1000);
    D d("ddddd thing", "davis", 123, 123123);
    a.name("alice");
    b.name("bob")->num(10);
    c.name("charlie")->bigNumber(123456789);
    d.name("delta")->thing("delta thing")->num(200)->bigNumber(123456); //
        // 4. request for member 'name' is ambiguous
    std::cout << a.name() << " " << b.name() << " " << b.num() << std::endl;
    std::cout << c.name() << " " << c.bigNumber() << std::endl;
    std::cout << d.name() << " " << d.num() << " "; //
        // 5. same as error 4
    std::cout << d.thing() << " " << d.bigNumber() << std::endl;
    return 0;
}

Other things that we don't understand:

  1. When we call, e.g., b.num(), there is no problem, but when we call b.name(), we need the line marked with the arrow and letter B. If it was inherited from A with return type A*, why would it not also be able to return B* if it is a derived class?

  2. Is doing something like B* name (std::string str) { m_name = str; return this; } even good practice? We feel that it is not but it really shortens stuff in our actual project due to the extremely high number of class members and methods. Perhaps we can use, instead of a pointer to the class, a reference to the object with B& name (std::string str) { m_name = str; return *this; }?

  3. How could we handle a more complex inheritance structure without running into conflicts like we are here? For example:

class A { /* ... */ };
class B : public A { /* ... */ };
class C : public A { /* ... */ };
class D : public B, public C { /* ... */ }; // therefore has functionality of A, B, C
class E : public C { /* ... */ }; // therefore has functionality of A, C, but not B
class F : public A, public E { /* ... */ }; therefore has functionality of A, C, E, but not B

Other tips on this messy code would be greatly appreciated.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • 2
    `std::string name() { return m_name; } ` --> `using A::name;`. `B* name (std::string str) { m_name = str; return this; }` returns the different type. – 273K Aug 31 '21 at 22:33
  • 1
    Please break this apart into _at least_ two questions (probably more) and post them separately. One specific question per question please. – Ted Lyngmo Aug 31 '21 at 22:38
  • Regarding second question, "good practice" is always in context. Using [covariant return types](https://en.cppreference.com/w/cpp/language/virtual#Covariant_return_types) is a thing, although that concept is rather exclusivve to `virtual` functions. It makes sense the way you used it (although `using A::name;` would be better than exact copy of the function). For the 3rd question, the term for duckduckgoing is "diamond problem". – Yksisarvinen Aug 31 '21 at 22:54

2 Answers2

1

For the first example, if you the call b.name("bob") will return a pointer to an instance of class A. You then take this instance and call the num() function. Class A doesn't have a num() function. That is something that has been added in the derived class B.

For questions 1 and 2:

You mention that name() is being inherited in class B from class A which it is, but not in the way you think. In class A you need to specify the name() function as virtual. This means that class B will be able to change the implementation of the function by overriding it. At the moment class B is creating its own name() function which is unrelated to the name() function in class A.

Once this is done you can use covariant return types to change the return type to a derived class and this should solve both 1 and 2.

For the third question:

The problem you're facing here is commonly referred to as the diamond problem and is a big topic in itself. There are ways of tackling it. Have a look at this post and this post.

Some other tips:

  1. If you're working with inheritance, make sure you understand the use of the virtual and override keywords.

  2. Take a look at the const keyword for some of your member functions. This shows that the functions do not mutate the class itself. This should be used for all member functions that do not mutate the class. Examples in your code would be the 'getter' functions for name() and num(). This is known as being const correct.

WalleyM
  • 172
  • 7
  • point 1 is at best misleading if not outright wrong -- B::name does not overload A::name, it overrides it. You can't overload things in different scopes, which brings in the second problem the OP has -- when you override a function it overrides all overloads in the base class. – Chris Dodd Aug 31 '21 at 22:53
  • My mistake. I did mean override. Have edited and fixed now. – WalleyM Sep 01 '21 at 00:01
  • Now it is wrong -- you can change the return type when overriding, and frequently you want to. – Chris Dodd Sep 01 '21 at 00:05
  • There are no virtual functions here, so there is no overriding. – Pete Becker Sep 01 '21 at 00:13
  • Have now removed my answers to 1 and 2. I assumed the original post intended to make the name() function virtual which is why I referred them to those keywords. – WalleyM Sep 01 '21 at 00:18
0

The reason you need line B is called covariant return typing -- it is fairly common when overriding or hiding a method for a derived class to want to have that method return a reference or pointer to the derived class rather than a reference or pointer to the base class. In C++ you can do that, but you need to do it explicitly.

The reason you need line A is that once you've overridden or hidden the name function in the derived class (added line B), it hides all the overloads of name in the base class. So if you still want to have the overload with no arguments, you need to add it back explicitly. If you just want to get all the overloads in the base class, you can say using A::name; in B and it will inherit all the name methods you haven't explicitly overridden

For part 2, when you want to initialize a field in a base class, you need to delegate it to the base class constructor. So you should have:

    B(std::string str, int x) : A(str), m_num(x)
    { }
Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • 1
    `name()` is not virtual, so there is no overriding. – Pete Becker Sep 01 '21 at 00:18
  • @PeteBecker: doesn't matter -- it still overrides the base class method. What `virtual` does is make it possble to call the derived method from a base pointer or reference -- without it, the method called depends only on the static type of the pointer or reference, but you can still call the derived method with a derived pointer. – Chris Dodd Sep 01 '21 at 00:23
  • 1
    No. A derived-class function with the same name and argument list as a virtual function in a base class overrides the base class function. That’s what “override” means. For a `B` object, `name()` hides anything in the base with the same name. That’s the only relationship between the base- and derived-class versions of `name`. – Pete Becker Sep 01 '21 at 00:59
  • If you want you can say "hide" instead of "override", but the words mean the same thing. – Chris Dodd Sep 01 '21 at 02:45
  • 1
    The C++ standard makes a very clear distinction between overriding and hiding. "A declaration of a name in a nested declarative region hides a declaration of the same name in an enclosing declarative region..." [basic.scope.hiding]/1. "If a virtual member function `vf` is declared in a class `Base` and in a class `Derived`, derived directly or indirectly from `Base`, a member function `vf` with the same name, parameter-type-list (9.2.3.5), cv-qualification, and ref-qualifier (or absence of same) as `Base::vf` is declared, then `Derived::vf` overrides `Base::vf`." [class.virtual]/2. – Pete Becker Sep 01 '21 at 13:01