Joining identical class interfaces

I have these two classes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
public:
    template <typename T>
    void foo(T &x){ /*...*/ }
    template <typename T>
    void bar(T &x){ /*...*/ }
    //etc.
};

class B{
public:
    template <typename T>
    void foo(T &x){ /*...*/ }
    template <typename T>
    void bar(T &x){ /*...*/ }
    //etc.
};
I would like to modify these two to have a Base class from which they both derive such that I can do
1
2
3
4
5
6
7
8
9
10
11
void do_foo(Base &b){
    int x;
    std::string s;
    b.foo(x);
    b.foo(s);
}

A a;
B b;
do_foo(a);
do_foo(b);
Now, obviously I can't just do
1
2
3
4
5
6
7
8
class Base{
public:
    template <typename T>
    virtual void foo(T &) = 0;
    template <typename T>
    virtual void bar(T &) = 0;
    //etc.
};
but is there any way I can expose a common template interface from Base such that the correct template in the implementation class will get called, without having to a) explicitly list all actual template parameters that are used in the program, b) encode the type of the implementation into the type of Base, for example as a template parameter or something equally dumb, or c) make do_foo() a template function. If someone can figure out a way to do this but it can't involve inheritance (e.g. it might involve std::function or switches or whatever), I'm fine with that too. I just want do_foo() to have a single unchanging interface even if I add more implementation classes.
So why do you want two identical classes?
Can't you just type-erase the template argument T somehow?
1
2
3
4
5
class Base {
public:
    virtual void foo(std::any) = 0;
    virtual void bar(std::any) = 0;
};


This really does seem like an XY problem. While I really enjoy contrived problems like this, it seems like the wrong way to solve whatever problem you have.

About the problem as stated:

I assume you realize this already, because of the list of points you made, but member function templates aren't instantiated until they are required. By the time a virtual call is dispatched, it is too late to instantiate the required template. (Hence why virtual templates don't exist.)

Somehow, a solution needs to instantiate every function it might require at compile-time. Once the member function templates are instantiated, it should be manageable to call the right one dynamically using some type-erasure technique. You'd need to tell the component about all the derived classes somewhere (in traits classes, for instance) so that it can instantiate the right things.

Last edited on
coder777: They're two different implementations of the same interface. They respond to the same commands, but they do different things.

mbozze: I'm writing a serializer generator that generates de/serialization code and node graph traversal code from a textual description of the classes involved. The way I want it to work from the point of view of the user is, you call a global function passing the object you want to serialize and a "serialization stream", which handles the low level data conversions to/from serial sequences, and the function figures everything out and calls the stream's functions in the correct order with the correct parameters so that the object can later be reconstructed verbatim.
So you might do, for example
1
2
std::ifstream file("config.json");
auto configuration = deserialize<Configuration>(JsonDeserializerStream(file));
or you might do
1
2
std::ifstream file("config.bin");
auto configuration = deserialize<Configuration>(BinaryDeserializerStream(file));

Now, a serialization stream has a bunch of template functions that implement the actual serialization code. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//BinaryDeserializerStream
template <typename It>
void serialize_sequence(It begin, It end, size_t length){
    this->serialize(nullptr, (wire_size_t)length);
    for (; begin != end; ++begin)
        this->serialize(nullptr, *begin);
}
template <typename T>
void serialize(const char *name, const std::vector<T> &v){
    this->serialize_sequence(v.begin(), v.end(), v.size());
}
template <typename T>
void serialize(const char *name, const std::set<T> &s){
    this->serialize_sequence(s.begin(), s.end(), s.size());
}

//...

//JsonDeserializerStream
template <typename It>
void serialize_sequence(const char *name, It begin, It end, size_t length){
    this->push(name);
    this->get_top() = nlohmann::json::array_t();
    for (; begin != end; ++begin)
        this->serialize("", *begin);
    this->merge_top();
}
template <typename T>
void serialize(const char *name, const std::vector<T> &v){
    this->serialize_sequence(name, v.begin(), v.end(), v.size());
}
template <typename T>
void serialize(const char *name, const std::set<T> &s){
    this->serialize_sequence(name, s.begin(), s.end(), s.size());
}
The reason these are templates is that these classes are not generated. They need to be able to handle types like 'GraphNode *', 'GraphNode **', 'std::vector<GraphNode>', 'std::vector<std::set<GraphNode>>', or whatever. I could of course generate these classes as well, but then debugging that would be a nightmare.
I'm writing a serializer generator that generates de/serialization code and node graph traversal code from a textual description of the classes involved.

Roll-your-own protoc?
https://developers.google.com/protocol-buffers/
Last edited on
Something like that. Protobuf and Avro have the limitation that they can't serialize arbitrary object graphs, which was something I needed when I started this. Cap'n Proto is better, but because it works by basically memory-mapping the serialized data, to prevent stack overflow attacks it has to impose an arbitrary recursion depth (IIRC, 50 levels by default) on the data structures.

Look, I'm not a newbie. I explored my alternatives. If I knew there's a serializer library for C++ that implements these kinds of features I wouldn't have wasted my time with this.
In fact, the first version of the project that first used my implementation was written in C# and used a C# wrapper around Protobuf that did support object graphs. I imagine the wrapper used reflection to walk the object graph and convert pointers to object IDs. When I rewrote it in C++ (IIRC I gave up trying to correctly implement disposable stream filters that I could be 100% sure were closed when they went out of scope) I this serializer because I didn't want to redesign the entire project.
Here's an idea:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class AbstractSerializer{
protected:
    virtual void serialize(const void *, std::uint32_t) = 0;
public:
    template <typename T>
    void serialize(const T &x){
        //I already have a generated implementation for get_type_id
        this->serialize((const void *)&x, get_type_id<T>::value);
    }
};

class BinarySerializer : public AbstractSerializer{
public:
    //(All the serialize overloads, but the class is still abstract.)
};

//This class is generated on a per-file basis.
class BinarySerializerForFileXXXXX : public BinarySerializer{
protected:
    void serialize(const void *x, std::uint32_t type_id) override{
        switch (type_id){
            case 1:
                this->BinarySerializer::serialize(*(int *)x);
                break;
            case 2:
                this->BinarySerializer::serialize(*(std::string *)x);
                break;
            case 3:
                this->BinarySerializer::serialize(*(std::vector<std::string> *)x);
                break;
            //...
            default:
                //This is just for the sake of defensiveness. The get_type_id use above works like
                //a static_assert to ensure that type_id will always have a valid value.
                throw std::runtime_error("Internal error: invalid type ID " + std::to_string(type_id));
        }
    }
};
Topic archived. No new replies allowed.