Какая обратная совместимость требует назначения для rvalues?

C++ Primer 5th:

(Код также из книги, контекст 99%, указанный здесь)

#include <string>
using namespace std;
int main()
{
    //no error
    string s1 = "123", s2 = "aaaa";
    s1 + s2 = "wow";
    auto a = (s1 + s2).find("a");
}

До нового стандарта (здесь он говорил C++ 11), не было никакого способа предотвратить такое использование. Чтобы поддерживать обратную совместимость, классы библиотек продолжают разрешать присвоение значениям rvalues. Однако мы могли бы предотвратить такое использование в наших собственных классах. В этом случае wed подобно тому, как заставить левый операнд (т.е. Объект, которому this указывает), быть lvalue.

Какая обратная совместимость требует назначения для rvalues?

Кстати, мне также интересно, почему s1 + s2 = "wow" разрешен, но int я = 3, j = 4; я + j = 7; int я = 3, j = 4; я + j = 7; не допускается. (Поскольку это тесно связано, я предпочитаю не открывать другой вопрос)

Ответы

Ответ 1

Это может быть антиклиматическое обоснованное предположение. Я буду приветствовать любой другой конкретный пример, однако, следуя общим правилам, представляется очень разумным.

  1. Такое ограничение не нарушит какой-либо конкретный код, но ограничит область принятых программ. c++ довольно консервативен, когда дело доходит до таких изменений, иногда к большой боли. Заметным примером будет наиболее неприятный анализ, какой код сломается, если A a(); был интерпретирован как построенный по умолчанию A? Тем не менее, это не так, чтобы быть обратно совместимым с синтаксисом c. Это довольно PIA для синтаксического анализа.

  2. c++ позволяет семантическому переопределению значения оператора для пользовательских типов. Я не знаю, есть ли хороший пример семантического переопределения для operator= но есть некоторые довольно примечательные примеры для других операторов. boost :: program_options "злоупотребляет" operator() довольно странным образом, чтобы создать сжатый синтаксис, а Eigen переопределяет семантику с запятой. Операторы-указатели-члены часто переопределяются, чтобы делать что-то нестандартное, поскольку они часто не используются по умолчанию. Поэтому иногда это полезно.

  3. Я думаю, это может быть полезно с классами, где operator= имеет побочные эффекты и не обязательно означает изменение значения в памяти. Я полагаю, что в некоторых встроенных разработках вы можете иметь Row и Col и вы пишете row * col = LED_ON или что-то в этом роде. Еще один пример из верхней части моей головы - библиотека выражений, например, operator<=> еще не существует, поэтому operator= можно использовать для оценки таких вещей, как (p ^ p) <=> p.

operator= является особенным среди операторов, и они не намного более особенны, чем другие функции-члены. Если вы напишете такой код:

#include <iostream>
using namespace std;

struct A{
friend ostream& operator<<(ostream& out, A& a) { out << "A "; return out;}
};    

int main() {
    A a1, a2;
    cout << a1=a2 << '\n';
    return 0;
}

Это... сломается. Это потому, что побитовый сдвиг имеет приоритет над =. Он требует скобок вокруг a1=a2. Это означает, что operator= действительно не имеет особых прав на языке.

Другое дело, что вы можете перегружать этих операторов почти так, как вы пожелаете, поэтому писать

#include <iostream>
using namespace std;

struct A{
    bool operator=(A& rhs) {return true;}
};    

int main() {
    A a1, a2;
    cout << (a1=a2) << '\n';
    return 0;
}

Совершенно законно. Язык предоставляет оператору синтаксические ярлыки и не намного больше. Я не думаю, что многие жалуются, что (a+b).method() работает, это то же самое для (a+b).operator=().

Бонус: пример с int не работает, потому что вы не можете перегружать операторы для примитивных типов, а значение по умолчанию определено так, что оно не принимает значения rvalues. Он демонстрирует поведение, которое вы, похоже, ожидаете. В основном мы лишены свободы переопределения семантики примитивных операторов.

Ответ 2

Вот пример кода, который в настоящее время отлично действует (даже предупреждение с Clang), и это сломается. Хитрость заключается в том, что operator + возвращает специальный объект с переопределенным operator =.

В этом примере переопределенный operator = устанавливает первый операнд сложения, создавшего специальный объект:

#include <iostream>

// A simple class containing an int with a special operator =
class A {
public:
    int val;

    class B operator + (const A& other);
};

/* A very special subclass keeping a ref on the first member
 * of the addition of A objects that created it. This ref
 * will be assigned by operator = */
class B: public A {
    A& ref;      // ref on the A object used at creation time
    // private ctor: B can only be created from A objects sums
    B(A& orig): ref(orig) {
        this->val = orig.val;
    }

public:
    B(const B& src): A(src), ref(src.ref)  {}  // copy ctor...
    // second part of the trick: operator = assigns ref
    B& operator = (const A& src) {
        ref = src;
        return *this;
    }
    friend class A;
};

B A::operator +(const A& other) {
    B ret(*this);
    ret.val += other.val;
    return ret;
}

int main(){
    A a = {1}, b= {2}, c= {5};
    A d;

    d = a + b = c;
    // a+b will eval to a B having val=3 and ref=a
    // a+b = c will set a.val to 5 AFTER ABOVE EVALUATION
    // d = a+b will set d.val to 3

    std::cout << a.val << " " << b.val << " " << c.val << " " << d.val << std::endl;
    // outputs as expected: 5 2 5 3

    return 0;
}

Хорошо, я не могу представить себе реальный пример использования, но программисты могут иметь странные идеи, если компилятор их принимает (ведь я мог представить это...). И я действительно думаю, что позволить operator + иметь побочные эффекты - это открытая дверь для операций, как то, что я показал.