5

Say I have a class that stores some data,

class Value {
public:
    enum class Type {
        int_type,
        float_type,
        double_type,
        bool_type
    };

    friend bool operator==(const Value& lhs, const Value& rhs) {
        //  how to make this function clean and concise?
    }

private:
    void* ptr;
    Type type;
};

ptr points to the underlying value and type indicates how the ptr should be cast.

To compare Value objects, I can definitely list all possible type combinations but code will be hard to maintain. like:

if (lhs.type == Type::int_type && rhs.type == Type::float_type) {
    return *static_cast<int*>(lhs.ptr) == *static_cast<float*>(rhs.type);
}

Any idea how to reduce complexity?

Update:

I want this class to be a dynamic type, which means I could do the following:

Value v("abc");       // v is now a string
v = 123;              // v is now an int
bool b = (v == 123.0); // b is true

So I don't think templates are what might be helpful.

Einiemand
  • 302
  • 1
  • 8
  • 2
    This is exactly what templates are for. If you don't use templates you have to use a `switch` statement. Decent compilers should warn you when you don't define all cases. – yyny Apr 12 '19 at 22:22
  • 1
    Yeah templates seems the way to go. Still I don't know whether you really want to compare an int with a float. – Dimfred Apr 12 '19 at 22:25
  • @YoYoYonnY You _never_ "have to use a switch statement" –  Apr 12 '19 at 22:25
  • 2
    Actually, templates may not be the way to go. It looks like this is a Variant. [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant) may get you want you want for free. – user4581301 Apr 12 '19 at 22:25
  • @user4581301 Well, std::variant heavily relies on templates, but I agree, `std::variant` seems like the standard solution. – yyny Apr 12 '19 at 22:32
  • Yes, `variant` is a big user of templates. When you broight templates up above, I figured you were suggesting a templated class that could only hold the type of data it was expanded around. It looks a lot like Einiemand doesn't know at compile time what type they'll get. – user4581301 Apr 12 '19 at 22:35
  • 1
    Einiemand you have described `std::variant`. I assume you are putting yourself through writing your own version as a learning experience? – user4581301 Apr 12 '19 at 22:49
  • `c++` is at it's core a statically typed language, so don't do this for convenience or as a general solution. If you have a specific situation and need this kind of functionality you should look at [std::variant](https://en.cppreference.com/w/cpp/utility/variant) and [std::any](https://en.cppreference.com/w/cpp/utility/variant). – super Apr 12 '19 at 22:49
  • 1
    @user4581301 No, I am just trying to implement some functionality. – Einiemand Apr 12 '19 at 22:51
  • 2
    @super Sadly the compiler I will use only supports c++11 – Einiemand Apr 12 '19 at 22:52
  • Interested in giving [Boost](https://theboostcpplibraries.com/boost.variant) a shot? – user4581301 Apr 12 '19 at 23:04
  • @user4581301 Haha, I'd prefer writing all those `switch...case...` stuff – Einiemand Apr 12 '19 at 23:06
  • It was worth a try. You might want to dig through [Code Review](https://codereview.stackexchange.com/) to see if anyone's asked about and gotten a more elegant solution that'll compile for C++11. – user4581301 Apr 12 '19 at 23:21

3 Answers3

2

In effect, what you are trying to do is write a weakly-typed type.

Such types underpin scripting languages such as python and javascript.

Writing a type like this is more complex than you might at first imagine.

However, it becomes easier once you have defined the matrix of conversions (e.g. what are the rules for comparing string to bool?)

Here's a start, using std::variant:

#include <variant>
#include <string>
#include <type_traits>
#include <algorithm>
#include <cassert>

//
// Step 1 - define your conversion rules
//          this is not as trivial as you might think
//

template<class To>
struct convert;

template<>
struct convert<int>
{
    template<class From>
    auto operator()(From const& from) const -> int
    {
        return int(from);
    }
    auto operator()(std::string const& from) const -> int
    {
        return std::atoi(from.c_str());
    }
};
template<>
struct convert<double>
{
    template<class From>
    auto operator()(From const& from) const -> double
    {
        return double(from);
    }
    auto operator()(std::string const& from) const -> double
    {
        return std::atof(from.c_str());
    }
};
template<>
struct convert<bool>
{
    template<class From>
    auto operator()(From const& from) const -> bool
    {
        return bool(from);
    }
    auto operator()(std::string  from) const -> bool
    {
        auto lcase = [](auto ch) { return std::tolower(ch); };
        std::transform(from.begin(), from.end(), from.begin(), lcase);
        if (from == "true" || from == "yes" || std::atoi(from.c_str()))
            return true;
        else
            return false;
    }
};
template<>
struct convert<std::string>
{
    template<class From>
    auto operator()(From const& from) const -> std::string
    {
        return std::to_string(from);
    }

    auto operator()(bool const& from) const -> std::string
    {
        auto result = std::string();
        if (from)
            result.assign("true");
        else
            result.assign("false");
        return result;
    }

    auto operator()(std::string const& from) const -> std::string const&
    {
        return from;
    }
};

//
// Step 2 - use a std::variant
//

struct Value 
{
    explicit Value(int arg): store_(arg) {}
    explicit Value(double arg): store_(arg) {}
    explicit Value(bool arg): store_(arg) {}
    explicit Value(std::string arg): store_(std::move(arg)) {}
    explicit Value(const char* arg): store_(std::string(arg)) {}

    friend bool operator==(const Value& lhs, const Value& rhs) 
    {
        auto compare = [](auto &&l , auto&& r) 
        { 
            using l_type = std::decay_t<decltype(l)>;
            auto conv = convert<l_type>();
            return l == conv(r);
        };
        return std::visit(compare, lhs.store_, rhs.store_);
    }

private:
    using storage_type = std::variant<int, double, bool, std::string>;

private:
    storage_type store_;
};

int main()
{
    auto vt = Value(true);
    auto vst = Value("true");
    assert(vt == vst);
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
1

Approach

First, let us consider all the pair-combinations of the primitive types corresponding enumerators of the enum class Value::Type. Since all enumerators of Value::Type are not explicitly initialized, the first enumerator has the value of 0, the second one has the value of 1, and so on. Using these zero-starting integers, we can label all the type combinations by the continuous zero-starting integers as follows:

Live DEMO

std::pair<int , int   >   -->  4*int_type  + int_type    = 4*0+0 = 0
std::pair<int , float >   -->  4*int_type  + float_type  = 4*0+1 = 1
std::pair<int , double>   -->  4*int_type  + double_type = 4*0+2 = 2
...                            ...
std::pair<bool, bool  >   -->  4*bool_type + bool_type   = 4*3+3 = 15

Next, we introduce the following static member function template Value::check which provides a generic comparison for each type combination:

template<class T>
static bool check(const Value& lhs, const Value& rhs)
{
    return *static_cast<typename T::first_type*>(lhs.ptr)
            == *static_cast<typename T::second_type*>(rhs.ptr);
}

For instance, if T = std::pair<int, float>, this becomes the comparison of int and float which you write in your post.

Then I would like to propose the following O(1) approach. At compile-time, we construct the following array which stores the pointer to function and arr[i] is the pointer to check<T> where T is the i-th type of the above type combinations:

using comp_f = bool(*)(const Value& lhs, const Value& rhs);
comp_f arr[16] = { &check<std::pair<int, int>>, &check<std::pair<int, float>>, ... };

At run-time, given Value& lhs and Value& rhs, we calculate the corresponding index and call the appropriately instantiated function check<T> as follows. This process can be done with O(1) complexity:

std::size_t idx = 4*static_cast<std::size_t>(lhs.type) 
                  + static_cast<std::size_t>(rhs.type); // 0 ~ 15.

return arr[idx](lhs, rhs);

Combinatorics

Now our problem is how we can simply construct all the type combinations. I have answered to the almost same question with this problem. In the current case, applying this method, all the possible combinations can be generated by the following struct Combinations (and max66's approach also would be possible). Please note that here I use std::index_sequence and thus this works in C++14 and over. But there are various way to implement std::index_sequence in C++11:

template<std::size_t I, class Tuple>
using pairing = std::pair<
                typename std::tuple_element<I/std::tuple_size<Tuple>::value, Tuple>::type,
                typename std::tuple_element<I%std::tuple_size<Tuple>::value, Tuple>::type>;

template <class T, class Is>
struct make_combinations;

template <class Tuple, std::size_t... Is>
struct make_combinations<Tuple, std::index_sequence<Is...>>
{
    using pairs = std::tuple<pairing<Is, Tuple>...>;
};

template<class ...Args>
struct Combinations
{
    using types_tuple = typename make_combinations
                         <std::tuple<Args...>,
                          std::make_index_sequence<(sizeof...(Args))*(sizeof...(Args))>
                         >::pairs;
};

Using this Combinations, we can generate the tuple of all the type combinations as Combinations<int, float, double, bool>::types_tuple.

Live DEMO


Comparator

In summary, Variable::operator== is implemented as follows. Here make_comparator generates the struct comparator at compile-time passing all the type combinations to it's template parameter. comparator also create the array of the pointer to function check<T> at compile-time. Thus the comparison of two Value would be done with O(1) complexity:

Live DEMO

template<std::size_t N, class T>
struct comparator {};

template<std::size_t N, class... Combs>
struct comparator<N, std::tuple<Combs...>>
{
    using comp_f = bool(*)(const Value& lhs, const Value& rhs);
    const comp_f arr[sizeof...(Combs)];

public:
    constexpr comparator() : arr{ &check<Combs>... }
    {}

    bool operator()(const Value& lhs, const Value& rhs) const
    {
        const std::size_t idx = N*static_cast<std::size_t>(lhs.type)
                                + static_cast<std::size_t>(rhs.type);

        return arr[idx](lhs, rhs);
    }
};

template<class... Ts>
static constexpr auto make_comparator()
{     
    return comparator<sizeof...(Ts), typename Combinations<Ts...>::types_tuple>();
}

friend bool operator==(const Value& lhs, const Value& rhs)
{
    constexpr auto comp = make_comparator<int, float, double, bool>();
    return comp(lhs, rhs);
}
Hiroki
  • 2,780
  • 3
  • 12
  • 26
1

Maybe you can simply write a method that'll convert the value of every used type into a double (a method with a single plain switch) and then compare two doubles in your comparision operator? Just like this:

private:
    double ToDouble() const
    {
        switch (type)
        {
        case Type::int_type: return *static_cast<int*>(ptr);
        case Type::float_type: return *static_cast<float*>(ptr);
        case Type::double_type: return *static_cast<double*>(ptr);
        case Type::bool_type: return *static_cast<bool*>(ptr) ? 1.0 : 0.0;
        }
    }
public:
    friend bool operator==(const Value& lhs, const Value& rhs)
    {
        return lhs.ToDouble() == rhs.ToDouble();
    }