C ++ оператор, перегружающий память вопрос - PullRequest
3 голосов
/ 20 сентября 2009

В c ++ вы можете создавать новые экземпляры класса как в куче, так и в стеке. При перегрузке оператора вы можете создавать экземпляры в стеке так, чтобы это имело смысл?

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

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

Любая информация будет полезна, спасибо

{EDIT} Я перегружаю оператор +. Прямо сейчас я использую этот код

Point Point::operator+ (Point a)
{
Point *c = new Point(this->x+a.x,this->y+ a.y);
return *c;
}

Я скептически относился к созданию экземпляра c следующим образом:

Point c(this->x + a.x, this->y, a.y);

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

Ответы [ 6 ]

12 голосов
/ 20 сентября 2009

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

struct SomeClass {
    int value;
};

SomeClass operator+(const SomeClass &lhs, const SomeClass &rhs) {
    SomeClass retval;
    retval.value = lhs.value + rhs.value;
    return retval;
}

или

class SomeClass {
    int value;
public:
    SomeClass operator+(const SomeClass &rhs) const {
        SomeClass retval;
        retval.value = this->value + rhs.value;
        return retval;
    }
};

или даже:

class SomeClass {
    int value;
public:
    SomeClass(int v) : value(v) {}
    friend SomeClass operator+(const SomeClass &lhs, const SomeClass &rhs) {
        return SomeClass(lhs.value + rhs.value);
    }
};

Затем компилятор беспокоится о том, где (в стеке) действительно хранится возвращаемое значение.

Например, он будет применять оптимизацию возвращаемого значения, если может, но в принципе то, что происходит, - это работа «как будто», которую вы выполняете, создает некоторое значение в стеке перегрузки вашего оператора, а затем при возврате это копируется в везде, где это должно быть рядом. Если вызывающая сторона назначает возвращаемое значение, оно копируется туда. Если вызывающая сторона передает его по значению в какую-либо другую функцию, она копируется туда, где соглашение о вызовах говорит, что это должно быть для того, чтобы быть этим параметром функции. Если вызывающая сторона получает ссылку на const, она копируется во временную папку, скрытую в стеке.

3 голосов
/ 20 сентября 2009

C ++: RAII и временные

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

Но вы игнорируете, что C ++ будет использовать временные объекты, необходимые. Вы должны узнать, когда компилятор создаст (и затем оптимизирует) временную переменную, чтобы ваш код работал.

Временные объекты

Обратите внимание, что ниже я описываю очень упрощенную «чистую» точку зрения на то, что происходит: компиляторы могут и будут выполнять оптимизацию, и среди них будут удалять бесполезные временные эффекты ... Но поведение остается тем же.

Целые

Давайте начнем медленно: что должно произойти, когда вы играете с целыми числами:

int a, b, c, d ;
// etc.
a = b + (c * d) ;

Код выше может быть записан как:

int a, b, c, d ;
// etc.
int cd = c * d ;
int bcd = b + cd ;
a = bcd ;

Параметры по значению

Когда вы вызываете функцию с параметром, передаваемым «по значению», компилятор делает ее временную копию (вызывая конструктор копирования). И если вы вернетесь из функции «по значению», компилятор снова сделает ее временную копию.

Давайте представим объект типа T. Следующий код:

T foo(T t)
{
   t *= 2 ;

   return t ;
}

void bar()
{
   T t0, t1 ;

   // etc.

   t1 = foor(t0) ;
}

можно записать в виде следующего встроенного кода:

void bar()
{
   T t0, t1 ;

   // etc.

   T tempA(t1)     // INSIDE FOO : foo(t0) ;
   tempA += 2 ;    // INSIDE FOO : t *= 2 ;
   T tempB(tempA)  // INSIDE FOO : return t ;

   t1 = tempB ;    // t1 = foo...
}

Таким образом, несмотря на то, что вы не пишете код, вызов или возврат из функции (потенциально) добавит много «невидимого кода», необходимого для передачи данных с одного уровня стека на следующий / предыдущий.

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

О вашем коде

Ваш код утечет: вы «новый» объект и не удаляете его.

Несмотря на ваши опасения, правильный код должен быть больше похож на:

Point Point::operator+ (Point a)
{
   Point c = Point(this->x+a.x,this->y+ a.y) ;
   return c ;
}

Который со следующим кодом:

void bar()
{
    Point x, y, z ;
    // etc.
    x = y + z ;
}

Создает следующий псевдокод:

void bar()
{
    Point x, y, z ;
    // etc.
    Point tempA = z ;  // INSIDE operator + : Point::operator+ (Point a)
    Point c = z ;      // INSIDE operator + : Point c = Point(this->x+a.x,this->y+ a.y) ;
    Point tempB = c ;  // INSIDE operator + : return c ;

    x = tempB ;        // x = y + z ;
}

О вашем коде, версия 2

Вы делаете слишком много временных. Конечно, компилятор, вероятно, удалит их, но тогда не нужно принимать небрежные привычки.

Вы должны как минимум написать код как:

inline Point Point::operator+ (const Point & a)
{
   return Point(this->x+a.x,this->y+ a.y) ;
}
2 голосов
/ 21 сентября 2009

У вас уже было несколько хороших ответов. Вот еще несколько моментов, которые я хотел бы добавить к ним:

  • Вам следует избегать копирования Point объектов. Поскольку они больше встроенных типов (из вашего кода я предполагаю, что они состоят из двух встроенных), на большинстве архитектур их копирование обходится дороже, чем их передача по ссылке. Это изменит ваш оператор на: Point Point::operator+ (Point&) (Обратите внимание, что вы должны скопировать результат, так как нет места, где он может храниться постоянно, чтобы вы могли передать ссылку на него. )
  • Однако, чтобы проверить компилятор, вы не облажались и не случайно изменили аргумент оператора, вы передаете его по const ссылке: Point Point::operator+ (const Point&).
  • Поскольку operator+() (кроме, например, operator+=()) также не меняет свой левый аргумент, вы должны также проверить это компилятором. Для бинарного оператора, который является функцией-членом, левый аргумент - это то, на что указывает указатель this. Чтобы сделать this константой в функции-члене, вы должны вставить const в конце сигнатуры функции-члена. Это делает это: Point Point::operator+ (const Point&) const. Теперь ваш оператор обычно называется const-correct.
  • Обычно, когда вы предоставляете operator+() для вашего типа, люди ожидают, что operator+=() также будет присутствовать, поэтому обычно вам следует реализовать оба варианта. Поскольку они ведут себя очень похоже, чтобы не быть лишним, вы должны реализовать один поверх другого. Самый простой и эффективный (и, следовательно, более или менее канонический) способ сделать это - реализовать + поверх +=. Это делает operator+() довольно простым для написания - и что еще более важно: в основном это выглядит одинаково для каждого типа, для которого вы его реализуете:

Поскольку operator+() стало довольно тривиальным, вы, вероятно, захотите inline. Тогда это будет результирующий код:

 inline Point Point::operator+ (const Point& rhs) const
 {
    Point result(this);
    result += a;
    return result;
 }

Это несколько основных синтаксических и семантических особенностей, с которыми (надеюсь) все читающие это согласятся. Теперь вот эмпирическое правило, которое я использую для своего кода и которое я считаю очень полезным, но с которым, вероятно, не все согласятся:

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

Причина последнего (возьмите operator+=() в качестве примера) довольно проста: чтобы изменить его, им может потребоваться доступ к внутренностям левого аргумента. А изменение внутренних объектов класса лучше всего выполнять через функции-члены.

Причины первого не так просты. Среди прочего, Скотт Мейерс написал отличную статью , объясняющую, что вопреки распространенному мнению, использование функций, не являющихся членами, часто фактически увеличивает инкапсуляцию. Но есть и тот факт, что для аргумента this функций-членов некоторые правила (неявные преобразования, динамическая диспетчеризация и т. Д.) Отличаются от правил для других аргументов. Поскольку вы хотите, чтобы оба аргумента обрабатывались одинаково, при некоторых обстоятельствах может оказаться удивительным, что к левой стороне применяются разные правила.

Код выглядит следующим образом:

 inline Point operator+ (const Point& lhs, const Point& rhs) const
 {
    Point result(lhs);
    result += rhs;
    return result;
 }

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

Реализация operator+=() оставлена ​​читателю в качестве упражнения. :)

0 голосов
/ 20 сентября 2009

Используя ваш код:

Point Point::operator+ (Point a)
{
    Point result(this->x+a.x,this->y+ a.y);
    return result;
}

Это будет хорошо работать.
В основном это создает результат локально (в стеке). Но оператор return копирует результат обратно в вызывающую точку (точно так же как int). Он использует конструктор копирования точек для копирования значения обратно в точку вызова.

int main()
{
    Point  a(1,2);
    Point  b(2,3);

    Point  c(a + b);
}

Здесь оператор + создает локальный стек. Это копируется обратно в точку вызова (конструктор для c) при возврате. Затем конструктор копирования для c используется для копирования содержимого в c.

Но вы думаете, что это кажется немного дорогостоящим при копировании. Технически да. Но компилятору разрешено оптимизировать лишние конструкции копирования (и все современные компиляторы очень хороши в этом).

Возвращаясь к вашему коду.

Point Point::operator+ (Point a)
{
    Point *c = new Point(this->x+a.x,this->y+ a.y);
    return *c;
}

Не делай этого. Здесь вы распределяете динамически, но вы копируете результат обратно в точку вызова (как описано выше с использованием конструктора копирования). Таким образом, к тому времени, когда управление возвращается к точке вызова, вы потеряли указатель и не можете отменить выделение памяти (таким образом, утечка памяти).

Разница между Java и C ++ заключается в том, что когда мы возвращаем указатели, мы используем умные указатели, чтобы помочь вызывающей стороне определить, кто отвечает за освобождение памяти (посмотрите, кто владеет указателем).

0 голосов
/ 20 сентября 2009

Вы правы, что данные в стеке непригодны для использования при выполнении функции. Тем не менее, вполне нормально возвращать копии данных в стеке (что вы и делаете). Просто убедитесь, что вы не возвращаете указатели на данные в стеке.

0 голосов
/ 20 сентября 2009

Данные и код являются ортогональными понятиями. Какая разница, чтобы код работал на объекте из кучи, а не в стеке? (при условии, что вы соблюдаете границы объекта в обоих случаях)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...