std::string class inheritance and tedious c++ overload resolution #2

63 views Asked by At

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.

1

There are 1 answers

6
SergeyA On

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