c++inheritancepolymorphismconditional-operatordynamic-cast

Derived class copy-constructor not entered in favor of copy-constructor taking parent class reference instead


Consider a hierarchy

base_dir -> base_dir_ext -> server
                       \--> location

with the following reduced code containing a method that, through inheritance, sometimes accepts a server, sometimes a location, and needs to instantiate a location from it. Using the following main(), everything works fine and the location copy-constructor is entered.

#include <iostream>

class base_dir
{
    // shared inherited member
    public:
        base_dir() { };
        virtual ~base_dir() { };
};

class base_dir_ext : public base_dir
{
    // even more shared inherited members
    public:
        base_dir_ext() { };
        virtual ~base_dir_ext() { };
        base_dir_ext(const base_dir_ext &other);
        base_dir_ext(const base_dir &other);
};

class server : public base_dir_ext
{
    public:
        server() { };
        ~server() { };
        server(const server &other);
        // server(const base_dir &other);
        server &operator=(const server &other);
};

class location : public base_dir_ext
{
    public:
        location() { };
        ~location() { };
        location(const location&) : base_dir_ext()
        {
            std::cout << "COPY-CTOR FOR LOCATION ENTERED" << std::endl;
        }

        location(const base_dir&)
        {
            std::cout << "COPY-CTOR FOR BASE_DIR ENTERED" << std::endl;
        }

        location(const server&) : base_dir_ext()
        {
            std::cout << "COPY-CTOR FOR SERVER ENTERED" << std::endl;
        }
};

base_dir *process_location(base_dir *parent)
{
    std::cout << "typeid: " << typeid(*(dynamic_cast<location*>(parent) ? dynamic_cast<location*>(parent) : parent)).name() << std::endl;
    location loc((dynamic_cast<location*>(parent) ? dynamic_cast<location&>(*parent) : dynamic_cast<server&>(*parent)));
    // process location and add it to `parent`
    return (parent);
}

int main()
{
    location loc;

    base_dir *parent = &loc;

    process_location(parent);
}

Output:

typeid: 8location
COPY-CTOR FOR LOCATION ENTERED

However, as you may have noticed, I had to comment out server::server(const base_dir &other), i.e. server's constructor that takes base_dir. When I uncomment it,

#include <iostream>

class base_dir
{
    // shared inherited member
    public:
        base_dir() { };
        virtual ~base_dir() { };
};

class base_dir_ext : public base_dir
{
    // even more shared inherited members
    public:
        base_dir_ext() { };
        virtual ~base_dir_ext() { };
        base_dir_ext(const base_dir_ext &other);
        base_dir_ext(const base_dir &other);
};

class server : public base_dir_ext
{
    public:
        server() { };
        ~server() { };
        server(const server &other);
        server(const base_dir &other);
        server &operator=(const server &other);
};

class location : public base_dir_ext
{
    public:
        location() { };
        ~location() { };
        location(const location&) : base_dir_ext()
        {
            std::cout << "COPY-CTOR FOR LOCATION ENTERED" << std::endl;
        }

        location(const base_dir&)
        {
            std::cout << "COPY-CTOR FOR BASE_DIR ENTERED" << std::endl;
        }

        location(const server&) : base_dir_ext()
        {
            std::cout << "COPY-CTOR FOR SERVER ENTERED" << std::endl;
        }
};

base_dir *process_location(base_dir *parent)
{
    std::cout << "typeid: " << typeid(*(dynamic_cast<location*>(parent) ? dynamic_cast<location*>(parent) : parent)).name() << std::endl;
    location loc((dynamic_cast<location*>(parent) ? dynamic_cast<location&>(*parent) : dynamic_cast<server&>(*parent)));
    // process location and add it to `parent`
    return (parent);
}

int main()
{
    location loc;

    base_dir *parent = &loc;

    process_location(parent);
}

a compiler error is generated:

$ c++ -Wall -Wextra -Werror main.cpp
main.cpp: In function 'base_dir* process_location(base_dir*)':
main.cpp:54:48: error: operands to ?: have different types 'location' and 'server'  location loc((dynamic_cast<location*>(parent) ? dynamic_cast<location&>(*parent) : dynamic_cast<server&>(*parent)));
                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp:54:48: note:   and each type can be converted to the other

My question is: why does this happen? Why all of a sudden server and location appear to be different classes? Don't they count as equivalent as they have the same parent and grandparent?

To get rid of this error, I tried removing dynamic cast of parent to server& type in process_location():

#include <iostream>

class base_dir
{
    // shared inherited member
    public:
        base_dir() { };
        virtual ~base_dir() { };
};

class base_dir_ext : public base_dir
{
    // even more shared inherited members
    public:
        base_dir_ext() { };
        virtual ~base_dir_ext() { };
        base_dir_ext(const base_dir_ext &other);
        base_dir_ext(const base_dir &other);
};

class server : public base_dir_ext
{
    public:
        server() { };
        ~server() { };
        server(const server &other);
        server(const base_dir &other);
        server &operator=(const server &other);
};

class location : public base_dir_ext
{
    public:
        location() { };
        ~location() { };
        location(const location&) : base_dir_ext()
        {
            std::cout << "COPY-CTOR FOR LOCATION ENTERED" << std::endl;
        }

        location(const base_dir&)
        {
            std::cout << "COPY-CTOR FOR BASE_DIR ENTERED" << std::endl;
        }

        location(const server&) : base_dir_ext()
        {
            std::cout << "COPY-CTOR FOR SERVER ENTERED" << std::endl;
        }
};

base_dir *process_location(base_dir *parent)
{
    std::cout << "typeid: " << typeid(*(dynamic_cast<location*>(parent) ? dynamic_cast<location*>(parent) : parent)).name() << std::endl;
    location loc((dynamic_cast<location*>(parent) ? dynamic_cast<location&>(*parent) : *parent));
    // process location and add it to `parent`
    return (parent);
}

int main()
{
    location loc;

    base_dir *parent = &loc;

    process_location(parent);
}

But then, while typeid() clearly outputs location type, location's const base_dir& constructor is invoked instead of its own copy-constructor. Why? The output:

typeid: 8location
COPY-CTOR FOR BASE_DIR ENTERED

Why does the only presence of server(const base_dir &other); ruin the ternary? I do not wish to remove this method since I want my classes to be versatile. Is there anything I miss about inheritance that doesn't let my code add up? I'd prefer to preserve the first version of code with the server constructor method uncommented: is that possible?


Solution

  • The important part of the error message is the last sentence:

    main.cpp:54:48: note:   and each type can be converted to the other
    

    I seems like you are not aware of what the conditional operator actually does. It is not a drop in replacment for an if - else. This will compile without issues (after adding server(const base_dir &other) back in):

    if (auto p = dynamic_cast<location*>(parent)) {
        location loc(*p);
        // process location and add it to `parent`
        return parent;
    } else {
        location loc(dynamic_cast<server&>(*parent));
        // process location and add it to `parent`
        return parent;
    }
    

    Live Demo

    However, this is still too complicated. Rather than doing such casts and then conditionally calling one or the other constructor. The constructor could take a reference to base and do the checks internally.

    The issue with the conditional operator is that the type of the conditional operator is the common type of the last two operands. So lets look closely:

    location loc((dynamic_cast<location*>(parent) ? dynamic_cast<location&>(*parent) : dynamic_cast<server&>(*parent)));
    

    Its location& and server&. A server can be converted to a location via location::location(const server&) and a location can be converted to a server via server::server(const base_dir &other). Hence the resulting error message. Without the server::server(const base_dir&) converting cosntructor the resulting type of the conditional operator is location&, which actually makes the use of it here pointless. You cannot use the conditional operator for dynamic casts like this.

    For more details I refer you to https://en.cppreference.com/w/cpp/language/operator_other and https://en.cppreference.com/w/cpp/types/common_type.


    Here is a simpler example to demonstrate how the conditional operator messes up your casting:

    #include <iostream>
    
    struct base {
        virtual ~base() {}
    };
    struct foo : base {};
    struct bar : base {
        bar() = default;
        bar(const foo&) {}
    };
    
    void what_is_it(const base&) {std::cout << "base"; }
    void what_is_it(const foo&) { std::cout << "foo"; }
    void what_is_it(const bar&) { std::cout << "bar"; }
    
    int main() {
        foo a;
        base& x = a;
        what_is_it( dynamic_cast<bar*>(&x) ? (std::cout << "true",dynamic_cast<bar&>(x)) : dynamic_cast<foo&>(x));
        std::cout << "\n";
    
        bar b;
        base& y = b;
        what_is_it( dynamic_cast<bar*>(&y) ? (std::cout << "true",dynamic_cast<bar&>(y)) : dynamic_cast<foo&>(y));
    }
    

    Output:

    bar 
    truebar
    

    Because for x the cast fails while for y the cast succeeds. Though in either case the conditional operator makes the result a bar& irrespective of the different type it is cast to before. The common type of bar& and foo& is bar&, because a foo& can be converted to a bar& (but not vice versa).