7

Okay, I did some research and apparently there are a lot of duplicate questions on SO on this topic, to name a few:

etc. But I cannot help but raise this again, because

  1. With auto-typed return value, I am literally duplicating the function body with the only difference being the const function qualifier.
  2. It is possible that const version and non-const version may return types that are totally incompatible from each other. In such cases, neither Scott Meyers's const_cast idiom, nor the "private const function returning non-const" technique would work.

As an example:

struct A {
    std::vector<int> examples;
    auto get() { return examples.begin(); }
    auto get() const { return examples.begin(); }
};  // Do I really have to duplicate?
    // My real-world code is much longer
    // and there are a lot of such get()'s

In this particular case, auto get() returns an iterator while auto get() const returns a const_iterator, and the two cannot be converted into each other (I know erase(it,it) trick does the conversion in this case but that's not the point). The const_cast hence does not work; even if it works, that requires the programmer to manually deduce auto type, totally defeating the purpose of using auto.

So is there really no way except with macros?

12
  • Possible duplicate of How do I remove code duplication between similar const and non-const member functions? I don't see how this is not answered in the other question.
    – Drise
    Commented Mar 20, 2018 at 17:21
  • @Drise in this case the technique doesn't seem to apply because ::const_iterator != const ::iterator
    – user5244399
    Commented Mar 20, 2018 at 17:23
  • Meyer's method clearly doesn't work for member functions returning different types in const and non-const versions, but I'd say that's a rather uncommon case, out of iterators (although I may be wrong). Not sure if it's possible to work out a general solution but maybe you could have a function to convert const_iterator to iterator (say unconst_iterator) and have a pattern like auto get() { return unconst_iterator(example, static_cast<const A &>(*this).get()); }.
    – javidcf
    Commented Mar 20, 2018 at 17:51
  • @jdehesa fair enough. In my case it's not iterator but something very close (two similar classes, one with const members). I could indeed write a type conversion between the two. Thanks for the advice, yet still hopefully awaiting more elegant answers.
    – user5244399
    Commented Mar 20, 2018 at 18:44
  • 1
    P0847 hopes to solve this problem Commented Mar 21, 2018 at 15:35

3 Answers 3

2

Ok, so after a bit of tinkering I came up with the following two solutions that allow you to keep the auto return type and only implement the getter once. It uses the opposite cast of what Meyer's does.

C++ 11/14

This version simply returns both versions in the implemented function, either with cbegin() or if you don't have that for your type this should work as a replacement for cbegin(): return static_cast<const A&>(*this).examples.begin(); Basically cast to constant and use the normal begin() function to obtain the constant one.

// Return both, and grab the required one
struct A
{
private:
    // This function does the actual getter work, hiding the template details
    // from the public interface, and allowing the use of auto as a return type
    auto get_do_work()
    {
        // Your getter logic etc.
        // ...
        // ...

        // Return both versions, but have the other functions extract the required one
        return std::make_pair(examples.begin(), examples.cbegin());
    }

public:
    std::vector<int> examples{ 0, 1, 2, 3, 4, 5 };

    // You'll get a regular iterator from the .first
    auto get()
    {
        return get_do_work().first;
    }

    // This will get a const iterator
    auto get() const
    {
        // Force using the non-const to get a const version here
        // Basically the inverse of Meyer's casting. Then just get
        // the const version of the type and return it
        return const_cast<A&>(*this).get_do_work().second;
    }

};

C++ 17 - Alternative with if constexpr

This one should be better since it only returns one value and it is known at compile time which value is obtained, so auto will know what to do. Otherwise the get() functions work mostly the same.

// With if constexpr
struct A
{
private:
    // This function does the actual getter work, hiding the template details
    // from the public interface, and allowing the use of auto as a return type
    template<bool asConst>
    auto get_do_work()
    {
        // Your getter logic etc.
        // ...
        // ...

        if constexpr (asConst)
        {
            return examples.cbegin();

            // Alternatively
            // return static_cast<const A&>(*this).examples.begin();
        }
        else
        {
            return examples.begin();
        }
    }

public:
    std::vector<int> examples{ 0, 1, 2, 3, 4, 5 };

    // Nothing special here, you'll get a regular iterator
    auto get()
    {
        return get_do_work<false>();
    }

    // This will get a const iterator
    auto get() const
    {
        // Force using the non-const to get a const version here
        // Basically the inverse of Meyer's casting, except you get a
        // const_iterator as a result, so no logical difference to users
        return const_cast<A&>(*this).get_do_work<true>();
    }
};

This may or may not work for your custom types, but it worked for me, and it solves the need for code duplication, although it uses a helper function. But in turn the actual getters become one-liners, so that should be reasonable.

The following main function was used to test both solutions, and worked as expected:

int main()
{    
    const A a;
    *a.get() += 1; // This is an error since it returns const_iterator

    A b;
    *b.get() += 1; // This works fine

    std::cout << *a.get() << "\n";
    std::cout << *b.get() << "\n";

    return 0;
}
6
  • 4
    Now you need 3 methods for every getter instead of 2. It didn't really get any simpler…
    – mindriot
    Commented Mar 20, 2018 at 20:21
  • 1
    But you don't duplicate the function body, keeping the implementations consistent. And changing it in one place doesn't mean you have to change the other, as was asked in the question. Also avoids macros as was requested.
    – Carl
    Commented Mar 20, 2018 at 22:08
  • 1
    Upvoted. I think this is a very fair solution :) Let me wait and see if there are better ones to come
    – user5244399
    Commented Mar 21, 2018 at 14:02
  • It should work in principle, although leaves the door open to UB without warning if get_do_work modifies the state of the object. I wouldn't even be able to tell for sure whether calling (non-const) .begin() on a const_cast-ed vector is guaranteed to be safe.
    – javidcf
    Commented Mar 21, 2018 at 16:27
  • Accepted. I'd like to point out a comment @jonathan-wakely had posted. It does seem that an existing proposal P0847 targets the exact case I am asking, so I assume there exists no elegant solution within the current standard.
    – user5244399
    Commented Mar 22, 2018 at 13:25
2
struct A {
    std::vector<int> examples;

private:
    template <typename ThisType>
    static auto
    get_tmpl(ThisType& t) { return t.examples.begin(); }

public:
    auto get()       { return get_tmpl(*this); }
    auto get() const { return get_tmpl(*this); }
};

How about the above? Yes, you still need to declare both methods, but the logic can be contained in a single static template method. By adding a template parameter for return type this will work even for pre-C++11 code.

0

Just a hypothetical solution, that I am thinking about applying every where once we will have concepts: using friend free function in place of member function.

//Forwarding ref concept
template<class T,class U>
concept FRef = std::is_same_v<std::decay_t<T>,std::decay_t<U>>;

struct A{
  std::vector<int> examples;

  friend decltype(auto) get(FRef{T,A}&& aA){
    return std::forward<T>(aA).examples.begin();
      //the std::forward is actualy not necessary here 
      //because there are no overload value reference overload of begin.
    }
  };