Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prefer calling const member function and fallback to non-const version?

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?

like image 749
Hank Avatar asked Sep 21 '21 03:09

Hank


People also ask

How is it possible to have both const and non-const version of a function?

There are legitimate uses of having two member functions with the same name with one const and the other not, such as the begin and end iterator functions, which return non-const iterators on non-const objects, and const iterators on const objects, but if it's casting from const to do something, it smells like fish.

Can a const member function call a non-const function?

Const member functions in C++ It is recommended to use const keyword so that accidental changes to object are avoided. A const member function can be called by any type of object. Non-const functions can be called by non-const objects only.

What will happen if a const object calls a non-const member function?

If the function is non-constant, then the function is allowed to change values of the object on which it is being called. So the compiler doesn't allow to create this chance and prevent you to call a non-constant function on a constant object, as constant object means you cannot change anything of it anymore.

Can a const function return a non-const reference?

If the thing you are returning by reference is logically part of your this object, independent of whether it is physically embedded within your this object, then a const method needs to return by const reference or by value, but not by non-const reference.

What is the difference between const and non-const member functions?

The object called by these functions cannot be modified. It is recommended to use const keyword so that accidental changes to object are avoided. A const member function can be called by any type of object. Non-const functions can be called by non-const objects only.

Can you call a non-const method from a const method?

Obviously you can't call a non- const method from a const method. Otherwise, const would have no meaning when applied to member functions. A const member function can change member variables marked mutable, but you've indicated that this is not possible in your case.

Should I use helper functions with const and non-const functions?

It's better to move as much common functionality as you can into helper functions, then have your const and non-const member functions each do as little work as they need to. In the case of a simple accessor like this, it's just as easy to return m_bar; from both of the functions as it is to call one function from the other.

Can we change the data member of a const function?

Any attempt to change the data member of const objects results in a compile-time error. When a function is declared as const, it can be called on any type of object, const object as well as non-const objects.


2 Answers

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)
like image 55
Sam Varshavchik Avatar answered Sep 27 '22 20:09

Sam Varshavchik


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

like image 28
cigien Avatar answered Sep 27 '22 18:09

cigien