0

I have multiple classes that look almost the same, do almost the same, but define and use different data and some of their functions work different (though they have the same name / signature).

class SomeClass1{

    class UniqueData1{};
    UniqueData1 data;
    static UniqueData1 static_data;

    void doStuff1() {doWhatever( data);}
    void doStuff2() {doUniqueStuff( data);}

    void doUniqueStuff(UniqueData1 _data) {doABC( _data); doCDE();}

};

class SomeClass2{

    class UniqueData2{};
    UniqueData2 data;
    static UniqueData2 static_data;

    void doStuff1() {doWhatever( data);}
    void doStuff2() {doUniqueStuff( data);}

    void doUniqueStuff(UniqueData2 _data) {doMNO( _data); doPQR();}

};

There are many functions like doStuff(), and many classes. How do I solve this without bunch of copy-paste? I was thinking about templates and virtual functions but could not come up with any solution so far.

Edit: some of the same-looking functions need to call the functions that do unique stuff.

Newline
  • 769
  • 3
  • 12
  • "same class, different data, some functions work different" - this is almost exactly describing the use case for templates. Can you list the specifics of how these classes should differ? – parktomatomi Nov 20 '19 at 19:06
  • @parktomatomi The UniqueData is different for each class. doUniqueStuff() also does different stuff for each class. There are many functions like doStuff() but they look the same. +most importantly, some doStuff() functions need to call doUniqueStuff(). I will edit the question – Newline Nov 20 '19 at 19:11
  • 1
    Honestly, more details needed. Ideally a [MCVE], and a concrete "problem" you want people to solve (not just "make better") – Yakk - Adam Nevraumont Nov 20 '19 at 19:13
  • *I have multiple classes that look almost the same, do almost the same, but define and use different data* -- Is one a Car and the other an Animal? They both "move", "make sounds", etc. The point is that if these classes have nothing in common in terms of utility, then creating a class hierarchy just for the sake of saving typing is not a good idea. – PaulMcKenzie Nov 20 '19 at 19:19
  • It almost looks like the two classes would follow the same "contract" if their two nested classes followed the same contract. If so, you can define an interface between the two nested classes, and then an interface for the two top-level classes, then you just have one class. – rossb83 Nov 20 '19 at 19:21
  • @rossb83 Not sure if I understand. I was thinking of making a base-class with virtual functions but the data is defined in the classes so cant use that in the base-class to implement any functions. – Newline Nov 20 '19 at 19:25

1 Answers1

1

Well, it's really hard to determine what the real problem you're trying to solve is. BUT, based on the bare minimum example you gave, this is how you might do it with templates:

template <int I>
class SomeClass {
public:
    class UniqueData{};
    UniqueData data;
    static UniqueData static_data;
    void doStuff() { doWhatever(data); }
    void doStuff2() { doUniqueStuff(data); }
    void doUniqueStuff(UniqueData uniqueData);
};

void doCDE() { }
void doABC(SomeClass<1>::UniqueData) { }
void doPQR();
void doMNO(SomeClass<2>::UniqueData) { }

template<>
void::SomeClass<1>::doUniqueStuff(SomeClass<1>::UniqueData data) {
    doABC(data);
    doCDE();
}

template<>
void::SomeClass<2>::doUniqueStuff(SomeClass<2>::UniqueData data) {
    doMNO(data);
    doPQR();
}

int main() {
    SomeClass<1>().doStuff2();
    SomeClass<2>().doStuff2();
}

Here, the template instance has its own unique datatype, and two functions. And these two functions are specialized to call different functions.

EDIT: You specified that you want UniqueData to have some unique members. If you're willing to be flexible, you can use tuples for that:

#include <functional>
#include <iostream>
#include <tuple>
#include <string>

template<typename T>
void doWhatever(T) { }

// This is now a variadic template. All arguments after id are packed into TUniqueDataMembers
template <int id, typename... TUniqueDataMembers>
class SomeClass {
public:
    // UnqiueData has a tuple member (with TUniqueDataMembers). It has one member of each template argument given
    class UniqueData {
    public:
        UniqueData() : data(std::make_tuple(TUniqueDataMembers{}...)) { }
        std::tuple<TUniqueDataMembers...> data;
    };
    UniqueData data;
    static UniqueData static_data;
    void doStuff() { doWhatever(data); }
    void doStuff2() { doUniqueStuff(data); }
    void doUniqueStuff(UniqueData uniqueData);
};

// Using a type definition saves some boilerplate, now that we're using multiple template arguments
using SomeClass1 = SomeClass<1, int>;
using SomeClass2 = SomeClass<2, double, std::string>;

void doCDE() { }
void doABC(SomeClass1::UniqueData data) {
    // Use std::get<index>(tuple) to access unique data
    std::cout << "int: " << std::get<0>(data.data) << "\n";
}
void doPQR() { }
void doMNO(SomeClass2::UniqueData data) {
    std::cout << "double: " << std::get<0>(data.data) << "\n";
    std::cout << "string: " << std::get<1>(data.data) << "\n";    
}

template<>
void SomeClass1::doUniqueStuff(SomeClass1::UniqueData data) {
    doABC(data);
    doCDE();
}

template<>
void SomeClass2::doUniqueStuff(SomeClass2::UniqueData data) {
    doMNO(data);
    doPQR();
}

int main() {
    SomeClass1 class1 {};
    std::get<0>(class1.data.data) = 42;
    class1.doStuff2();

    SomeClass2 class2 {};
    std::get<0>(class2.data.data) = 2.5;
    std::get<1>(class2.data.data) = "hello, world!";
    class2.doStuff2();
}

EDIT: Regarding the constraint that UniqueData must be an ENUM with different members.... okay, you got me there. Templates can't do name substitution. Macros are the only thing that can do that. Unfortunately, C++ macros don't offer the ability to recurse like variadic templates can.

BUT, using variadic macro tricks: Overloading Macro on Number of Arguments You can get an "up-to-N" argument solution that you can expand as you need. Here is the implementation for up to 4 enum members:

#include <functional>
#include <iostream>
#include <tuple>
#include <string>

template<typename T>
void doWhatever(T) { }

// Variadic macro trick for "up-to-N-argument macros"
#define _GEN_ENUM1(_0) enum class UniqueData { _0 };
#define _GEN_ENUM2(_0, _1) enum class UniqueData { _0, _1 };
#define _GEN_ENUM3(_0, _1, _2) enum class UniqueData { _0, _1, _2 };
#define _GEN_ENUM4(_0, _1, _2, _3) enum class UniqueData { _0, _1, _2, _3 };
#define _GET_GEN_ENUM(_0, _1, _2, _3, NAME,...) NAME
#define _GEN_ENUM(...) _GET_GEN_ENUM(__VA_ARGS__, _GEN_ENUM4, _GEN_ENUM3, _GEN_ENUM2, GEN_ENUM1)(__VA_ARGS__)

// Macro to generate class variadic parameters pass to _GEN_ENUM
#define DECL_SOMECLASS(id, ...)\
    class SomeClass##id{\
    public:\
        _GEN_ENUM(__VA_ARGS__)\
        UniqueData data;\
        static UniqueData static_data;\
        void doStuff() { doWhatever(data); }\
        void doStuff2() { doUniqueStuff(data); }\
        void doUniqueStuff(UniqueData uniqueData);\
    };

// Macro to implements SomeClass<N>::doStuff2() with custom function
#define IMPL_SOMECLASS(id, f_do_unique, f_do_not_unique)\
    void SomeClass##id::doUniqueStuff(SomeClass##id::UniqueData data) {\
        f_do_unique(data);\
        f_do_not_unique();\
    }

// Actually declare the classes
DECL_SOMECLASS(1, A, B, C)
DECL_SOMECLASS(2, D, E)

void doABC(SomeClass1::UniqueData data) {
    std::cout << "A: " << (int)SomeClass1::UniqueData::A << "\n";
    std::cout << "B: " << (int)SomeClass1::UniqueData::B << "\n";
    std::cout << "C: " << (int)SomeClass1::UniqueData::C << "\n";
    std::cout << "Data: " << (int)data << "\n";
}
void doCDE() { }
void doMNO(SomeClass2::UniqueData data) {
    std::cout << "D: " << (int)SomeClass2::UniqueData::D << "\n";
    std::cout << "E: " << (int)SomeClass2::UniqueData::E << "\n";
    std::cout << "Data: " << (int)data << "\n";
}
void doPQR() { }

IMPL_SOMECLASS(1, doABC, doCDE)
IMPL_SOMECLASS(2, doMNO, doPQR)

int main() {
    SomeClass1 class1 {};
    class1.data = SomeClass1::UniqueData::C;
    class1.doStuff2();

    SomeClass2 class2 {};
    class2.data = SomeClass2::UniqueData::E;
    class2.doStuff2();
}

NOTE: by using macros instead of templates, you lose all type safety for the arguments of DECL_SOMECLASS and IMPL_SOMECLASS. So you can pass "lol" as the id and witness horrifying error messages. Here be dragons.

parktomatomi
  • 3,851
  • 1
  • 14
  • 18
  • Thanks! One problem is that UniqueData has to be defined in the class, as in SomeClass::UniqueData. But the definition doest necessarily look the same for each class. Only their name is the same. Like: `struct UniqueData{int a; float b;} data;` and `struct UniqueData{bool c;} data;` in an other class – Newline Nov 20 '19 at 19:33
  • Data like UniqueData can't be specialized / defined outside the class like you did with doUniqueStuff(), am I right? – Newline Nov 20 '19 at 19:48
  • Kind of. You can't make named members, but there are tools like `std::tuple` that can turn template arguments into data of different amounts and types. – parktomatomi Nov 20 '19 at 19:57
  • In my actual case, they are diferrent enums. Same names but different enumerators. Like `enum UniqueData { A,B,C}` and `enum UniqueData { M, N}`. – Newline Nov 20 '19 at 20:14
  • 1
    You're correct, templates can't solve that problem for you. Macros can get you kind of there with a lot of caveats. Edited answer to define enums with macros. – parktomatomi Nov 20 '19 at 21:55