C++ concept assignable_from won't accept const&-returning operator=

Viewed 147

In the following MCVE, std::assignable_from reports that A<double>& can't be assigned to from A<double> -- when it clearly can.

#include <iostream>

template <typename T>
class A
{
public:
    const A& operator= (const A& other)    {  return *this;    }
};

int main ()
{
    A<double> d1, d2; d1 = d2; //this works fine

    std::cout << std::boolalpha
        << "Is double assignable?    " << std::assignable_from<double&, double> << '\n' //Says true, as it should
        << "Is A<double> assignable? " << std::assignable_from<A<double>&, A<double>> << '\n'; //Says false

    return 0;
}

I know why. assignable_from expects operator= to have a return type of A&, not const A&.

template <class _LTy, class _RTy>
concept assignable_from = is_lvalue_reference_v<_LTy>
    && common_reference_with<const remove_reference_t<_LTy>&, const remove_reference_t<_RTy>&>
    && requires(_LTy _Left, _RTy&& _Right) {
        { _Left = static_cast<_RTy&&>(_Right) } -> same_as<_LTy>;
    };

Is there an alternative, short of writing my own concept for assignability? I've always had = return const &, because I thought it was dumb to say (A=B)=C.

2 Answers

I've always had = return const &, because I thought it was dumb to say (A=B)=C.

You are free to do that, but there are consequences when you break with established conventions. You have encountered one of them. std::assignable_from requires that you follow established C++ conventions for assignment operators. And this includes returning a modifiable reference to the assigned type in your operator= overload.

It should also be noted that if you return a const& from the assignment operator, you cannot =default it. C++ is very serious about returning modifiable references from assignment operators. It's an expected part of the language.

And writing your own concept for assignment won't help when you have to pass your type to a conceptualized function/class that is constrained on std::assignable_from or any constraint that uses it.

So just follow C++'s conventions.

There is indeed a problem with assignment operator and specifically the implicitly defined one, and also the increment and decrement operator as they are implemented, for historical reasons, in the standard library. It looks like the community has forgotten this old debate. You are right to look for a limitation of the assignment operator "reachability", but the way you do it is not the way it used to be advised.

First let's consider the problem:

#include <vector>

std::vector <int> f();

void g(std::vector <int> v) {
    decltype (auto) v1 = f() = v; //(1)
    decltype (auto) it = ++v.begin(); //(2)
    v1. size() //undefined behavior;
    ++it;      //undefined behavior;
}

The problem here is that the assignment operator and the preincrement operator are returning a reference to a temporary materialization. But the lifetime of those temporaries end at the end of their respective full-expressions (1) and (2).

The way to fix the problem is to ref qualify those operators.

#include <concepts>

struct A {
         A& operator = (const A&) & 
               =default;
         A& operator = (A&&) & 
               =default;

         //unfortunately, it implies verbosity:
         A(const A&) =default;
         A(A&&) = default;
         A() = default;

         A& operator ++ () &;
         };

static_assert (std::assignable_from <A&, A>);
static_assert (!std::assignable_from <A, A>);
Related