7

Consider a class Bar in two versions of a library:

/// v1
class Bar
{
  void get_drink()
  {
    std::cout << "non-const get_drink() called" << std::endl;
  }
};


/// v2
class Bar
{
  void get_drink()
  {
    std::cout << "non-const get_drink() called" << std::endl;
  }

  void get_drink() const
  {
    std::cout << "const get_drink() called" << std::endl;
  }
};

On the client side code, we own a Bar object and would like to get_drink. I want to be able to prefer calling the const version of get_drink() if it's available (when using v2 library) and fallback to the non-const version (when using v1 library). That is:

Bar bar;

bar.get_drink(); // this does not call the const version of v2

static_cast<const Bar&>(bar).get_drink(); // this does not compile against v1 library

Unfortunately, the library is not versioned and there's no other way to distinguish between the two versions.

I reckon some template magic has to be used. So the question is how?

Hank
  • 310
  • 1
  • 5
  • I suppose renaming the `const`-tagged method to `get_drink_const()` would be considered cheating? :) – Jeremy Friesner Sep 21 '21 at 03:48
  • Why don't call the const method from the non const method in v2? This is widely used in comparison operators for example. – 273K Sep 21 '21 at 03:52
  • Why not simply wrap the 2 versions in `#if(n)def`s, and then `#define` the version you are compiling against on any given build? – Remy Lebeau Sep 21 '21 at 03:54

2 Answers2

2

This seems to work:

#include <type_traits>
#include <iostream>

template<typename T, typename=void>
struct find_a_drink {

    void operator()(T &t) const
    {
        t.get_drink();
    }
};

template<typename T>
struct find_a_drink<T,
            std::void_t<decltype( std::declval<const T &>()
                      .get_drink())>
            > {

    void operator()(T &t) const
    {
        static_cast<const T &>(t).get_drink();
    }
};

template<typename T>
void drink_from(T &&t)
{
    find_a_drink<std::remove_reference_t<T>>{}(t);
}

/// v1
class Bar1
{
public:
  void get_drink()
  {
    std::cout << "non-const get_drink() called (Bar1)" << std::endl;
  }
};

class Bar2
{
public:
  void get_drink()
  {
    std::cout << "non-const get_drink() called (Bar2)" << std::endl;
  }

  void get_drink() const
  {
    std::cout << "const get_drink() called (Bar2)" << std::endl;
  }
};

int main()
{
    Bar1 b1;
    Bar2 b2;

    drink_from(b1);
    drink_from(b2);
    return 0;
}

Result:

non-const get_drink() called (Bar1)
const get_drink() called (Bar2)
Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
2

This is the same underlying idea as Sam's answer but using expression sfinae, which I think is clearer

// 1
template<typename T>
auto drink_from(T &t) -> decltype(static_cast<const T&>(t).get_drink())
{
    static_cast<const T &>(t).get_drink();
}

// 2
template<typename T>
auto drink_from(T &&t)
{
    t.get_drink();
}

Overload 1 is preferred if the argument t can be cast to a const and have get_drink called on it, which requires the existence of a const-qualified member. If such a member is not available, overload 2 is called, which calls the non-const-qualified member.

Bar b;
drink_from(b);  // calls const qualified member if available

demo

cigien
  • 57,834
  • 11
  • 73
  • 112