0

I have an issue similar to the following:

When is static cast safe when you are using multiple inheritance?

multiple inheritance: unexpected result after cast from void * to 2nd base class

but I really don't understand why it does not work, since I'm doing what is suggested (cast back to the original type).

Also, templates are involved.

I have a generic container of templated objects which I implement as a map of std::string IDs and void*:

std::map<std::string, void*> mymap;

The objects look like follows:

template <class T>
class A {
   virtual void f1(const T&) = 0;
};

class T1 {};
class T2 {};

class B : public A<T1> {
   void f1(const T1&) override;
} 

class D {
   virtual void f3 () {};
}

class C : public A<T2>,
          public D
{
   void f1(const T2&) override;
}

In the main code, I have something like this to add my objects to the map and call the appropriate method depending on the type:

template <class T>
void addClass(A<T>& a, std::string id){
    std::pair<std::string, void*> pair(id, (void*)&a);
    mymap.insert(pair);
}

template<class T>
void callback(A<T>&a, std::string typeID) {
    static_cast<A<T>*>(mymap[typeID])->f1();
}

The string unambiguously identifies what class is being used as a template, so that the callback can cast back to the correct type.

Everything worked just fine as long as I passed to addClass objects like B, i.e. with single inheritance from the pure virtual templated class A.

As soon as I pass an object like C, i.e. with multiple inheritance from A and D, the callback where I do the casting produces a SEGFAULT, even though I have no compilation errors.

UPDATE. Apparently, the problem was in the std::shared_ptr use rather than in the casting itself. Here is the MCVE.

classes.hpp:

#pragma once
#include <iostream>
#include <map>

template <class T>
class A {
public:
   virtual void f1(const T&) = 0;
};

class T1 {
public:
    double t1 = 10.0;
};
class T2 {
public:
    short int t2 = 8;
};

class B : public A<T1> {
public:
   void f1(const T1&) override;
};

class D {
public:
   virtual void f3();
};

class C : public A<T2>,
          public D
{
public:
   void f1(const T2&) override;
};

class MyContainer{
public:
    std::map<std::string, void*> mymap;

    template<class T>
    void addClass(A<T>& t, std::string id);

    template<class T>
    void callback(T& t, std::string id);
};

template<class T>
void MyContainer::addClass(A<T> &a, std::string id){
    std::pair<std::string, void*> pair(id, (void*)&a);
    mymap.insert(pair);
}

template<class T>
void MyContainer::callback(T& t, std::string id){
    static_cast<A<T>*>(mymap[id])->f1(t);
}

classes.cpp:

#include <classes.hpp>

void B::f1(const T1& t1){
    std::cout << "Hello from B using t1: " << t1.t1 << std::endl;
}

void C::f1(const T2 & t2){
    std::cout << "Hello from C using t2: " << t2.t2 << std::endl;
}

void D::f3() {
    std::cout << "Hello from D" << std::endl;
}

main.cpp:

#include <iostream>
#include <classes.hpp>
#include <memory>
using namespace std;

int main()
{
    std::shared_ptr<B> b(new B()); // inherits from A<T1>
    std::shared_ptr<C> c(new C()); // inherits from A<T2> and D


    MyContainer container;
    // no need to specify the template,
    // it is implict from the class being passed
    container.addClass(*b, "t1");
    container.addClass(*c, "t2");

    T1 t1;
    T2 t2;

    container.callback(t1, "t1");
    container.callback(t2, "t2");

}

If i replace the shared pointer with actual objects, everything is fine:

Hello from B using t1: 10
Hello from C using t2: 8

But in my original code I need the shared pointers because there are some conditionals at runtime to whether build them or not...

CMakeLists.txt:

cmake_minimum_required(VERSION 2.8)
add_compile_options(-std=c++11)
project(casting_problem)
include_directories(include)
add_library(${PROJECT_NAME} SHARED classes.cpp)
add_executable(${PROJECT_NAME}_exe "main.cpp")
target_link_libraries(${PROJECT_NAME}_exe ${PROJECT_NAME})
mcamurri
  • 153
  • 11
  • Have you defined `T1,T2` somewhere else? If not, they are going to be undefined, right? What happens when you write `class T1 {}; class T2{};`? – tmaric Jan 15 '19 at 14:19
  • 2
    Create a [mcve]. – eerorika Jan 15 '19 at 14:19
  • T1 and T2 are well defined, yes. I tried to simplify the example question because the true code is too complicated to show entirely. – mcamurri Jan 15 '19 at 14:21
  • 1
    @MarcoCamurri don't show your code entirely. Create a MCVE instead. – eerorika Jan 15 '19 at 14:21
  • The problem is that you lost the original type when casting to `void*`, so you would need it to be able to cast back to `A` because the cast would not be the same depending on the inheritance hierarchy. You have no issue with `B` because the underlying structure of `A` and `B` are likely similar, but within `C`, you have the vtable of `D` that mess your structure, so the cast fails. The `B` version which is working might be UB actually, but I am not sure... – Holt Jan 15 '19 at 14:26
  • I've added the MVCE. Apparently there is something odd with the shared pointers. Any help on that? – mcamurri Jan 15 '19 at 15:08
  • 1
    Looks like you are re-inventing [std::any](https://en.cppreference.com/w/cpp/utility/any) – Victor Gubin Jan 15 '19 at 18:12

1 Answers1

1

The problem with your code is that your shared pointers are null, and therefore behaviour of indirecting through them is undefined.

You can create shared objects using std::make_shared.

Keep in mind however, that the shared objects are destroyed as soon as last shared pointer is destroyed, at which point any pointers to the objects in your map (if any) are left dangling.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Oh...I forgot to initialize them in the example...I have now corrected the code but however it became...no more a MCVE because it does not segfault anymore (but my original code does). So isn't there anything strange or unsafe in what is currently written by now? – mcamurri Jan 15 '19 at 17:51
  • 1
    @MarcoCamurri With the shared pointers initialised, there is no longer undefined behaviour in the example. The whole idea is still unsafe to some degree, since it is easy to accidentally call `callback` with an argument of type that doesn't match the string argument, in which case the behaviour will be undefined. But as long as the type matches, it works. – eerorika Jan 15 '19 at 18:40
  • I found out what the problem was. It had nothing to do with the casting. It was exactly what happens here wrong init list order: https://geidav.wordpress.com/2013/05/09/a-pitfall-with-initialization-lists-in-c/ – mcamurri Jan 16 '19 at 09:42