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. Commented Mar 20, 2018 at 17:21
  • @Drise in this case the technique doesn't seem to apply because ::const_iterator != const ::iterator 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()); }. 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. Commented Mar 20, 2018 at 18:44
  • 1
    P0847 hopes to solve this problem Commented Mar 21, 2018 at 15:35

3 Answers 3

3
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.

Sign up to request clarification or add additional context in comments.

Comments

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; } 

7 Comments

Now you need 3 methods for every getter instead of 2. It didn't really get any simpler…
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.
Upvoted. I think this is a very fair solution :) Let me wait and see if there are better ones to come
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.
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.
|
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. } }; 

Comments