Search code examples
c++templatesgenerics

Generic function to compare objects of different classes in "depth"?


Is it possible to write a generic function that can compare objects of different classes which do not define operator==, something like:

template<typename T>
bool Compare(T obj1, T obj2) {
    ...?...
}

class Class1 {
    int i;
    int *pi;
};

class Class2 {
    char c;
    char *pc;
    std::string *ps;
};

Class1 a1, b1;
Initialise...
bool c1 = Compare(a1, b1);

Class2 a2, b2;
Initialise...
bool c2 = Compare(a2, b2);

Where the values pointed to are also compared, i.e.:

*a1.pi == *b1.pi

If I compared the memory blocks of the two objects, I would get something like:

a1.pi == b1.pi

Solution

  • Well, first things first.

    • As stated in the comments of the original question, I'm not sure the question has an interest for multiple reasons (main is that operator== is the right way to do it).
    • I do not recommend using code below
    • I do not recommend to attempt to fix the code below -- just read it (or not) and drop it)

    However, it's an occasion to come up with some fun templates, so I couldn't resist.

    C++ has no reflection

    Therefore, we cannot do a.GetFields() and loop over it, as we would in C# or Java.

    However, we still need a way to get fields and "enumerate" them somehow. Since C++ is strongly typed, we also need field types and can't rely on some "runtime magic" as we would in Javascript.

    That means we need a type list. Many implementations exist, but we just need basic functionalities so here is the dumbest possible version :

    #pragma once
    #include <cstdint>
    
    template <typename... Ts>
    struct type_list;
    
    namespace internal {
        template <size_t i, typename T, typename... Ts>
        struct type_at {
            static_assert(i < sizeof...(Ts) + 1, "index out of range");
            using type = typename type_at<i - 1, Ts...>::type;
        };
    
        template <typename T, typename... Ts> struct type_at<0, T, Ts...> {
            using type = T;
        };
    
        template <size_t i, typename... Ts>
        using type_at_t = typename type_at<i, Ts...>::type;
    }
    
    template <typename... Ts>
    struct type_list {
        static constexpr size_t length = sizeof...(Ts);
    
        template <size_t index>
        using at = internal::type_at_t<index, Ts...>;
    };
    
    template <>
    struct type_list<> {
        static constexpr size_t length = 0;
    };
    

    Then, we need a way to get fields in our "comparable classes". That means our classes will get a function which returns ith field. But, again, C++ is strongly typed so we can't have auto get_field(int x) since return type is different for different indices.

    Let's wrap everything in opaque pointers (void*) :

    
    template<typename FieldType>
    union opaque_field {
        FieldType x;
        void* opaque;
    };
    

    and start with a simple class:

    class A {
        friend struct GetFields<A>; // only addition to make in your business code
    private:
        int _x;
        double _y;
        A* _next;
    public:
        A(int x, int y, A* next = nullptr) {
            this->_x = x;
            this->_y = y;
            this->_next = next;
        }
    };
    

    And let's implement a fake reflection :

    
    template<typename T>
    struct GetFields;
    
    template<typename T>
    struct FieldTypes;
    
    
    template<>
    struct FieldTypes<A> {
        using type = type_list<int, double, A*>;
    };
    
    template<>
    struct GetFields<A> {
        static void* get_field(const A& a, size_t index) {
            switch (index) {
            case 0:
                return opaque_field<int> { a._x }.opaque;
            case 1:
                return opaque_field<double> { a._y }.opaque;
            case 2:
                return opaque_field<A*> { a._next }.opaque;
            default:
                return nullptr;
                // do something (throw ?)
            }
        }
    };
    

    this ugly logic will have to be implemented for all "comparable" types (not scalar / pointers / arithmetic though). Again, why not just write a good old operator==, but let's continue having fun with c++.

    Template comparison Once done, we can't implement a template compare function with following properties :

    • returns false if parameters are not of the same type
    • compare arithmetic and enum values directly
    • dereference pointers to compare content (better not using c like memory allocation)
    • on compound types (struct or classes), compare fields one by one.
    
    template<typename T1, typename T2, typename E = void>
    struct compare_helper {
        static bool compare(const T1& x, const T2& y) {
            return false;
        }
    };
    
    template<typename T>
    struct compare_helper<T, T, std::enable_if_t<
        (std::is_enum_v<T> || std::is_arithmetic_v<T>) && !std::is_pointer_v<T>>> {
        static bool compare(const T& x, const T& y) {
            return x == y;
        }
    };
    
    
    template<typename T>
    struct compare_helper<T*, T*> {
        static bool compare(T* x, T* y) {
            if (x == nullptr && y == nullptr) {
                return true;
            }
            if ((x == nullptr && y != nullptr) || (y == nullptr && x != nullptr)) {
                return false;
            }
            return compare_helper<T, T>::compare(*x, *y);
        }
    };
    
    template<typename T>
    struct compare_helper<T, T, std::enable_if_t<
        (!std::is_pointer_v<T> && !std::is_enum_v<T> && !std::is_arithmetic_v<T>)
        >> {
    
        // oh the beautiful compile time loop because we can't do a for loop
        template<size_t index, size_t stop>
        struct inner {
            using field_type = typename FieldTypes<T>::type::template at<index>;
            static bool func(const T& x, const T& y) {
                field_type left = opaque_field<field_type>{ .opaque = GetFields<A>::get_field(x, index) }.x;
                field_type right = opaque_field<field_type>{ .opaque = GetFields<A>::get_field(y, index) }.x;
                return compare_helper<field_type, field_type>::compare(left, right) && inner<index + 1, stop>::func(x, y);
            }
        };
    
        template<size_t stop>
        struct inner<stop, stop> {
            static bool func(const T& x, const T& y) {
                return true;
            }
        };
    
        static bool compare(const T& x, const T& y) {
            return inner<0, FieldTypes<T>::type::length>::func(x, y);
        }
    };
    
    // final wrap in a template function for ease of use
    template<typename T1, typename T2>
    bool Compare(const T1& x, const T2& y) {
        return compare_helper<T1, T2>::compare(x, y);
    }
    

    Then, it's quite straightforward to use :

    int main() {
        A* a = new A(1, 2);
        A* b = new A(1, 2, a);
        A* c = new A(1, 2, a);
    
        printf("%d\n", Compare(a, b));
        printf("%d\n", Compare(b, c));
        return 0;
    }
    

    Complete example here.