0

Previous question: std::string class inheritance and tedious c++ overload resolution

In steps after the previous question i have tried to test operator+ over the raw string pointer: "aaa" + path_string{ "bbb" }. And found it does not call the respective path_string class friend function.

I tried to add not template overload operator+ (2) but it didn't work either. But i've found that the templated one (3) did work.

#include <string>

template <class t_elem, class t_traits, class t_alloc>
class path_basic_string : public std::basic_string<t_elem, t_traits, t_alloc>
{
public:
    using base_type = std::basic_string<t_elem, t_traits, t_alloc>;

    path_basic_string() = default;
    path_basic_string(const path_basic_string & ) = default;
    path_basic_string(path_basic_string &&) = default;

    path_basic_string & operator =(path_basic_string path_str)
    {
        this->base_type::operator=(std::move(path_str));
        return *this;
    }

    path_basic_string(base_type r) :
        base_type(std::move(r))
    {
    }

    path_basic_string(const t_elem * p) :
        base_type(p)
    {
    }

    base_type & str()
    {
        return *this;
    }

    const base_type & str() const
    {
        return *this;
    }

    using base_type::base_type;
    using base_type::operator=;

    // ... all over operators are removed as not related to the issue ...

    // (1)
    friend path_basic_string operator+ (const t_elem * p, const base_type & r)
    {
        path_basic_string l_path = p;
        l_path += "xxx";
        return std::move(l_path);
    }

    friend path_basic_string operator+ (const t_elem * p, base_type && r)
    {
        if (!r.empty()) {
            return "111" + ("/" + r); // call base operator instead in case if it is specialized for this
        }

        return "111";
    }

    // (2)
    friend path_basic_string operator+ (const t_elem * p, path_basic_string && r)
    {
        base_type && r_path = std::move(std::forward<base_type>(r));
        if (!r_path.empty()) {
            return "222" + ("/" + r_path); // call base operator instead in case if it is specialized for this
        }

        return "222";
    }

    // (3) required here to intercept the second argument
    template <typename T>
    friend path_basic_string operator+ (const t_elem * p, T && r)
    {
        base_type && r_path = std::move(std::forward<base_type>(r));
        if (!r_path.empty()) {
            return "333" + ("/" + r_path); // call base operator instead in case if it is specialized for this
        }

        return "333";
    }
};

using path_string = path_basic_string<char, std::char_traits<char>, std::allocator<char> >;

std::string test_path_string_operator_plus_right_xref(path_string && right_path_str)
{
    return "aaa" + right_path_str;
}

int main()
{
    const path_string test =
        test_path_string_operator_plus_right_xref(std::move(path_string{ "bbb" }));
    printf("-%s-\n", test.str().c_str());

    return 0;
}

The output for 3 compilers: gcc 5.4, clang 3.8.0, msvc 2015 (19.00.23506)

-333/bbb-

https://rextester.com/BOFUS59590

As i remembered the C++ standard clarifies this like as a templated function has to be lookup only when no one not templated function is matched arguments exactly. But the (2) operator has to be matched exactly, but why then it is not even called?

If remove (3) then the (1) would call instead of (2) which is matches better than (1).

What is going on here?

PS: I think this is the same issue around the const + single reference like from previous question.

Andry
  • 2,273
  • 29
  • 28
  • 1
    Note that `std::move(std::forward(...))` is redundant, `std::move(...)` should give the same result. You always get an rvalue reference either way. And in the context of your operators, neither `std::move` nor `std::forward` do anything. It's not clear to me where the confusion lies exactly, but you misunderstand how move semantics work. – François Andrieux Nov 06 '18 at 16:41
  • @FrançoisAndrieux `std::forward` casts a reference to a reference to a different type (base class in this case), then `std::move` changes a forwarded reference to a rvalue reference, eh? Could be simply `static_cast(r)` probably though. – bipll Nov 06 '18 at 16:49
  • @bipll I'm not sure what you are asking. But `base_type` is never a reference type, so `std::forward` doesn't perform any perfect forwarding (`T` is ignored completely). Adding `std::move` guaranties you would ignore perfect forwarding by always moving (and failing to compile if you can't). Edit : to respond to the edit to your comment, that cast is essentially just `std::move`. – François Andrieux Nov 06 '18 at 16:53
  • my question is `why (3) calls instead of (2)`? – Andry Nov 06 '18 at 16:56
  • @FrançoisAndrieux I wrote so just to explicitly state i want a move over the base type instead of derived. – Andry Nov 06 '18 at 17:00
  • 1
    What's a reference to a reference? – Lightness Races in Orbit Nov 06 '18 at 17:04
  • Aside from the issue at hand - how sure are you that your code will never experience slicing issues? This all looks extremely prone to accidental misuse (both inside and outside your influence). – Max Langhof Nov 06 '18 at 17:05
  • @MaxLanghof slicing is not a problem here, i won't store anything in the derived class anyway. – Andry Nov 06 '18 at 17:22
  • @FrançoisAndrieux I'm not asking anything. The variable cast is of reference type `T &&`, which is either `U &&` or `U &`, where `U` is `std::decay_t` and is a subclass of `base_type`. By such chain of calls it is finally cast to `base_type &&`. – bipll Nov 07 '18 at 00:17

1 Answers1

2

In following snippet:

std::string test_path_string_operator_plus_right_xref(path_string && right_path_str)
{
    return "aaa" + right_path_str;
}

right_path_str is a non-const l-value. Because of that, it can never bind to an rvalue reference.

When template overload is not available, it binds to const lvalue reference overload in

friend path_basic_string operator+ (const t_elem * p, const base_type & r)

and when template is there, it's a better match for non-const lvalue reference:

template <typename T>
friend path_basic_string operator+ (const t_elem * p, T && r)

In this case, T&& is a forwarding reference, which collapses to non-const lvalue reference. To fix your code, make sure to move from rvalue reference arguments when calling functions and make it a habit - whenever you pass an rvalue reference below, use std::move, and whenever you pass forwarding reference, use std::forward.

std::string test_path_string_operator_plus_right_xref(path_string && right_path_str)
{
    return "aaa" + std::move(right_path_str);
}
SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • If i understant you correctly the templated `T &&` can binds to both either `const blabla &` or `blabla &&`, when `blabla &&` only to `blabla &&`. Is that correct? – Andry Nov 06 '18 at 17:12
  • @Andry The templated `T&&` can bind to anything (keyword: forwarding references). It is different from a `path_string&&`, which only binds to temporaries. – Max Langhof Nov 06 '18 at 17:13
  • @MaxLanghof Why then it does not bind to itself? The function argument type is `path_string &&`. – Andry Nov 06 '18 at 17:15
  • 1
    The function argument _is not_ of type `path_string&&`, it _binds to_ something of type `path_string&&`. The argument itself is an lvalue, because it has a name. Yes, this distinction is quite confusing at first. – Max Langhof Nov 06 '18 at 17:16
  • @Andry an l-value can never bind to rvalue reference. Period. And in `Foo&& f` f is an l-value unless Foo is a template argument to a function. Reiterating: rvalue references are lvalues! – SergeyA Nov 06 '18 at 17:17
  • @SergeyA In other words it must be templated to bind to r-value, right? It makes sence. – Andry Nov 06 '18 at 17:19