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?

  • Possible duplicate of [How do I remove code duplication between similar const and non-const member functions?](https://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-func) I don't see how this is not answered in the other question. – Drise Mar 20 '18 at 17:21
  • @Drise in this case the technique doesn't seem to apply because `::const_iterator != const ::iterator` –  Mar 20 '18 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`](https://stackoverflow.com/q/765148) (say `unconst_iterator`) and have a pattern like `auto get() { return unconst_iterator(example, static_cast(*this).get()); }`. – jdehesa Mar 20 '18 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. –  Mar 20 '18 at 18:44
  • Do you have access to C++ 17 @Pigpag ? – Carl Mar 20 '18 at 18:54
  • It seems your only concrete objection to the Meyer's style solution is that the two functions "may return types that are totally incompatible from each other." In such a case, where's the duplication of the function body? – Mike Kinghan Mar 20 '18 at 20:25
  • @Carl unfortunately only [tag:C++14] –  Mar 21 '18 at 14:04
  • @MikeKinghan just read my example above. with `auto` return type, this is totally possible. –  Mar 21 '18 at 15:27
  • 1
    [P0847](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0847r0.html) hopes to solve this problem – Jonathan Wakely Mar 21 '18 at 15:35
  • Also possibly related (but not a duplicate): https://stackoverflow.com/questions/2150192/how-to-avoid-code-duplication-implementing-const-and-non-const-iterators – Adrian McCarthy Mar 21 '18 at 16:59
  • I think this issue is generally overdramatized. Unless you have fairly complex getter methods, just copy pasting the code is usually more readable easier to analyze and easier to maintain than jumping to all kinds of hoops and writing boilerplate code to prevent code duplication. The resulting code might have fewer redundancies, but it tends to be longer and more complex overall. – MikeMB Mar 25 '18 at 09:07
  • @MikeMB Yes, I agree. Copying a bit of code is not that bad in most cases. Nevertheless I just find that the current standard might have missed something to fully take advantage of auto type inferrence. A lot of things are being added, so eventually people may realize that "now that I have A, I really don't need B anymore - let's make the standard evolve", which causes a chain reaction. –  Apr 10 '18 at 18:46

3 Answers3

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;
}
Carl
  • 2,057
  • 1
  • 15
  • 20
  • 4
    Now you need 3 methods for every getter instead of 2. It didn't really get any simpler… – mindriot Mar 20 '18 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 Mar 20 '18 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 –  Mar 21 '18 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. – jdehesa Mar 21 '18 at 16:27
  • Accepted. I'd like to point out a comment @jonathan-wakely had posted. It does seem that an existing proposal [P0847](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0847r0.html) targets the exact case I am asking, so I assume there exists no elegant solution within the current standard. –  Mar 22 '18 at 13:25
  • @Carl: I'm not saying it doesn't answer the question, but I sometimes have to wonder if "solutions" such as this don't result in much bigger maintenance problems than minor code duplication. – MikeMB Mar 25 '18 at 09:09
1
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.

ayurchen
  • 386
  • 3
  • 9
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.
    }
  };
Oliv
  • 17,610
  • 1
  • 29
  • 72