Может ли перегрузка оператора работать без ссылок? - PullRequest
6 голосов
/ 04 ноября 2011

По словам Бьярне Страуструпа, ссылки на C ++ были добавлены для поддержки перегрузки операторов:

Ссылки были введены в первую очередь для поддержки перегрузки операторов.

C передает каждый аргумент функции по значению, и когда передача объекта по значению будет неэффективной или неуместной, пользователь может передать указатель. Эта стратегия не работает там, где используется перегрузка оператора. В этом случае удобство нотации является существенным, поскольку нельзя ожидать, что пользователи будут вставлять операторы адресов, если объекты большие. Например:

a = b - c;

является приемлемым (то есть обычным) обозначением, но

a = &b - &c;

нет. В любом случае, &b - &c уже имеет значение в C, и я не хотел его менять.

Наличие как указателей, так и ссылок на языке является постоянным источником путаницы для новичков в C ++.

Не мог ли Бьярне решить эту проблему, введя специальное правило языка, позволяющее аргументам объекта распадаться на указатели, если существует пользовательская операторная функция, которая принимает такие указатели?

Объявление и использование вычитания выглядело бы тогда:

Foo operator-(const Foo* x, const Foo* y);
a = b - c;

Было ли такое решение когда-либо предложено / рассмотрено? Будут ли в этом какие-то серьезные недостатки?

Да, я знаю, ссылки предоставляют другие преимущества из-за их ограничений, но это не главное.

Интересно, что оператор присваивания C с классами, похоже, работал именно так:

Изменение значения присваивания для объектов класса [...] выполняется путем объявления функции-члена класса с именем operator=. Например:

class x {
public:
    int a;
    class y * p;
    void operator = (class x *);
};

Ответы [ 5 ]

7 голосов
/ 04 ноября 2011

IME, автоматическое преобразование является проклятием C ++ . Давайте все напишем благодарные письма Бьярне Страуструпу за то, что он не добавил широкое автоматическое преобразование объекта в указатель.

Если вы ищете техническую причину: в C ++ вычитание указателей, даже указателей на пользовательские типы, уже четко определено. Кроме того, это может привести к неоднозначности с перегруженными версиями операторов, которые принимают объекты за копию.

5 голосов
/ 04 ноября 2011

Я не понимаю, как это решает проблему: operator- вызов по указателям уже имеет значение.

Вы бы определили operator- для Foo* аргументов, но это уже существует.

Тогда вам потребуется некоторая надуманная семантика, которая "при явном вызове с указателями вызывается арифметический оператор указателя. При вызове с объектными значениями l они распадаются на указатели, а затем вызывается перегруженная версия". Что, честно говоря, кажется гораздо более надуманным и гораздо менее интуитивным, чем просто добавление ссылок.

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

Вы предлагаете неявное преобразование, которое имеет другую семантику, чем выполнение преобразования самостоятельно. То есть:

Foo foo;
bar(foo); 

выполняет неявное преобразование из Foo в Foo*, после чего вызывается функция с подписью void bar(Foo*).

Но этот код сделал бы что-то совершенно другое:

Foo foo;
bar(&foo);

, который также преобразуется в Foo*, и также вызывает функцию с сигнатурой void bar(Foo*), но потенциально это может быть другая функция. (Например, в случае operator- один будет оператором, перегруженным пользователем, а другой - стандартным арифметическим указателем.

А затем рассмотрите код шаблона. В целом, неявные преобразования делают шаблоны действительно болезненными, поскольку передаваемый тип может не соответствовать типу, с которым вы хотите работать.

Конечно, есть и более практические проблемы:

Ссылки позволяют оптимизировать, что невозможно с указателями. (Компилятор может предположить, что они никогда не равны нулю, и он может сможет лучше выполнять анализ псевдонимов)

Кроме того, код будет молча не работать, если не найден соответствующий operator-. Вместо того, чтобы сказать , что , компилятор неявно начнет выполнять арифметику с указателями. Не нарушитель соглашения, но я действительно предпочитаю, чтобы ошибки, по возможности, выявлялись рано.

И одна из целей C ++ состояла в том, чтобы сделать его универсальным: избежать «магических» типов или функций. В реальном C ++ операторы - это просто функции с другим синтаксисом. С этим правилом у них также была бы другая семантика. (Что если оператор вызывается с синтаксисом функции? operator+(a, b)?

2 голосов
/ 04 ноября 2011

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

Чтобы более конкретно решить вашу проблему, люди, изучающие C ++, могут сначала запутаться из-за ссылок и указателей, но я не уверен, что им что-то разъяснит, если такое автоматическое преобразование происходит от их имени. Например:

Foo ** operator+(Foo **lhs, Foo **rhs)
{...}

Foo *varFoo1,*varFoo2;
varFoo1 + &varFoo2;

Следуя гипотетическому неявному объекту-указателю, должен ли varFoo1 быть принят в качестве аргумента для метода, ожидающего Foo **? Поскольку метод ожидает указатель на Foo * и varFoo1 * is-a * Foo *.

Другое преимущество ссылки, используемой в качестве аргументов:
константные ссылки могут получить значение r в качестве аргумента (например, классический строковый литерал), в то время как указатели не могут.

0 голосов
/ 04 ноября 2011

Я никогда не читал книгу D & E, но, насколько я понимаю, ссылки не были добавлены, чтобы он выглядел лучше при передаче аргументов в функцию, но чтобы он выглядел лучше при получении значений из них. Как простые, так и составные операторы присваивания и нижние индексы приводят к значению lvalue при использовании встроенных типов, но невозможно сделать то же самое со встроенными типами без ссылок.

Мне также интересно, как бы вы реализовали оператор, который работает как с типом, так и с указателем на тип.

struct Foo { };
struct Bar { };
Foo operator +(Foo&, Bar*);
Bar operator +(Foo*, Bar&);
Foo operator +(Bar*, Foo&);
Bar operator +(Bar&, Foo*);

против

struct Foo { };
struct Bar { };
Foo operator +(Foo*, Bar*);
Bar operator +(Foo*, Bar*);
Foo operator +(Bar*, Foo*);
Bar operator +(Bar*, Foo*);

То же самое можно показать только одним типом, но просто сказать «не делай этого» кажется разумным в этом случае ... когда задействовано несколько типов, вероятность случайного введения неоднозначности возрастает.

0 голосов
/ 04 ноября 2011

Если вы согласны с тем, что выполнение этого с указателями (неявным или явным образом) приведет к вводящей в заблуждение семантике (работаю ли я с указателем или с указателем?), Тогда без ссылок остается только вызов по значению. (Помимо вашего Foo c = &b - &a; примера рассмотрим случай, когда вы хотели написать оператор, который действительно использовал указатель в качестве одного из аргументов)

Я не думаю, что указатели без & на сайте вызовов полезны и, конечно, не лучше, чем ссылки. Если вы сделаете это особой функцией operator s и только операторов, то это хорошо перенесет поведение в область «загадочного» особого случая. Если вы хотите уменьшить его «особый случай», сделав его общей функцией, то я не думаю, что он полезен или полезен для вызова по ссылке в его нынешнем виде.

Например, если я хочу написать оператор, который принимает Foo и void*, я могу написать:

Foo operator+(const Foo& f, void *ptr);

В соответствии с вашими предлагаемыми правилами, которые станут: Foo operator+(Foo *f, void *ptr);. Проблема, как я вижу, будет заключаться в том, что, хотя я хотел, чтобы ptr явно был указателем по «правилу implict &», он принимал бы что угодно , и, похоже, способ для меня, чтобы запретить автоматическое преобразование. Так что double d; Foo f; f = f + d; будет соответствовать этому, как и int i; Foo f; f = f + i;, потенциально двумя разными способами!

Вызов по значению мог бы работать и иметь смысл для «простых» типов, и вы могли бы использовать интеллектуальные указатели для случаев, когда вам действительно не нужно брать копию каждого операнда. В целом, хотя идея принуждения к копированию всего выглядит очень нечистой по сравнению со ссылочным подходом.

...