Постинкремент C ++: объекты против примитивных типов - PullRequest
0 голосов
/ 20 февраля 2019

Мы не можем использовать предварительное увеличение для значений r:

int i = 0;
int j = ++i++; // Compile error: lvalue required

Если мы определим класс:

class A
{
public:
    A & operator++()
    {
        return *this;
    }
    A operator++(int)
    {
        A temp(*this);
        return temp;
    }
};

, тогда мы можем скомпилировать:

A i;
A j = ++i++;

В чем разница между объектом и типом данных int, которые

j = ++i++;

компилирует с A, но не с int?

Ответы [ 2 ]

0 голосов
/ 20 февраля 2019

Это происходит потому, что когда перегруженные операторы определены как функции-члены, они следуют некоторой семантике, которая больше связана с вызовом функции-члена, а не с поведением встроенного оператора.Обратите внимание, что по умолчанию, если мы объявляем нестатическую функцию-член, такую ​​как:

class X {
public:
    void f();
    X g();
};

, тогда мы можем вызывать ее как для выражений типа класса lvalue, так и для rvalue:

X().f();   // okay, the X object is prvalue
X x;
x.f();     // okay, the X object is lvalue
x.g().f(); // also okay, x.g() is prvalue

При перегрузкеразрешение для выражения оператора выбирает функцию-член, выражение изменяется как простой вызов этой функции-члена, поэтому оно следует тем же правилам:

++A(); // okay, transformed to A().operator++(), called on prvalue
A a;
++a;   // okay, transformed to a.operator++(), called on lvalue
++a++; // also technically okay, transformed to a.operator++(0).operator++(),
       // a.operator++(0) is a prvalue.

Этот вид неэквивалентности между встроеннымиоператоры и перегруженные операторы также происходят с левым подвыражением присваивания: бессмысленное утверждение std::string() = std::string(); допустимо, но утверждение int() = int(); недопустимо.

Но вы отметили в комментарии «Я хочу создатькласс, который мешает ++a++ ".Есть как минимум два способа сделать это.

Во-первых, вы можете использовать оператор, не являющийся членом, вместо члена.Большинство перегруженных операторов могут быть реализованы как члены или не члены, где тип класса должен быть добавлен как дополнительный тип первого параметра функции, не являющейся членом.Например, если a имеет тип класса, выражение ++a попытается найти функцию, как если бы она была a.operator++(), а также функцию, как если бы она была operator++(a);и выражение a++ будет искать функции для выражений a.operator++(0) или operator++(a, 0).

(Этот способ использования обоих способов не применим к функциям с именами operator=, operator(), operator[] или operator->, поскольку они могут быть определены только как нестатические функции-члены, но не как не-члены. Функции с именами operator new, operator new[], operator delete или operator delete[] плюс пользовательский литералфункции, имена которых начинаются как operator "", следуют совершенно другим наборам правил.)

И когда аргумент класса соответствует параметру реальной функции, а не "неявному параметру объекта" нестатической функции-члена,тип ссылки, используемый в параметре, если он есть, как обычно, определяет, может ли аргумент быть lvalue, rvalue или любым из них.

class B {
public:
    // Both increment operators are valid only on lvalues.
    friend B& operator++(B& b) {
        // Some internal increment logic.
        return b;
    }
    friend B operator++(B& b, int) {
        B temp(b);
        ++temp;
        return temp;
    }
};

void test_B() {
    ++B(); // Error: Tried operator++(B()), can't pass
           // rvalue B() to B& parameter
    B b;
    ++b;   // Okay: Transformed to operator++(b), b is lvalue
    ++b++; // Error: Tried operator++(operator++(b,0)), but
           // operator++(b,0) is prvalue and can't pass to B& parameter
}

Другой способ - добавить квалификаторы ref в функции-члены, которыебыли добавлены к языку в версии C ++ 11 как особый способ контроля того, должен ли неявный аргумент объекта-члена быть lvalue или rvalue:

class C {
public:
    C& operator++() & {
        // Some internal increment logic.
        return *this;
    }
    C operator++(int) & {
        C temp(*this);
        ++temp;
        return temp;
    }
};

Примечание& между списком параметров и началом тела.Это ограничивает функцию принятием только lvalue типа C (или чего-то, что неявно преобразуется в C& ссылку) в качестве неявного аргумента объекта, аналогично тому, как const в том же месте позволяет неявному аргументу объектаиметь тип const C.Если вы хотели, чтобы функция требовала lvalue, но при желании это значение может быть const, const ставится перед квалификатором ref: void f() const &;

void test_C() {
    ++C(); // Error: Tried C().operator++(), doesn't allow rvalue C()
           // as implicit object parameter
    C c;
    ++c;   // Okay: Transformed to c.operator++(), c is lvalue
    ++c++; // Error: Tried c.operator++(0).operator++(), but
           // c.operator++(0) is prvalue, not allowed as implicit object
           // parameter of operator++().
}

Чтобы заставить operator= действоватьбольше как это делается для скалярного типа, мы не можем использовать функцию, не являющуюся членом, потому что язык допускает только объявления operator= членов, но квалификатор ref будет работать аналогично.Вам даже разрешено использовать синтаксис = default;, чтобы компилятор генерировал тело, даже если функция не объявлена ​​точно так же, как неявно объявленная функция присваивания.

class D {
public:
    D() = default;
    D(const D&) = default;
    D(D&&) = default;
    D& operator=(const D&) & = default;
    D& operator=(D&&) & = default;
};

void test_D() {
    D() = D(); // Error: implicit object argument (left-hand side) must
               // be an lvalue
}
0 голосов
/ 20 февраля 2019

Это ... просто есть.Есть несколько ограничений, которые применяются только к примитивным типам, а не к типам классов (ну, вы нашли самый очевидный!).

Это в основном потому, что операторы для встроенных типов - это одно, тогда какдля классов они являются просто скрытыми функциями-членами и, следовательно, совершенно другим зверем.

Это сбивает с толку?Я не знаю;возможно.

Есть ли действительно веская причина для этого?Я не знаю;возможно нет.У примитивных типов есть определенная инерция: зачем менять то, что было в C, только потому, что вы вводите классы?Какая польза от этого?С другой стороны, не будет ли слишком строгим запретить его для классов, чья реализация operator++ могла бы сделать то, о чем вы, как дизайнер языков, не думали?

...