С++:Копировать конструктор:Использовать геттеры или напрямую обращаться к переменным-членам?

StackOverflow https://stackoverflow.com/questions/1490225

Вопрос

У меня есть простой класс-контейнер с конструктором копирования.

Рекомендуете ли вы использовать геттеры и сеттеры или напрямую обращаться к переменным-членам?

public Container 
{
   public:
   Container() {}

   Container(const Container& cont)          //option 1
   { 
       SetMyString(cont.GetMyString());
   }

   //OR

   Container(const Container& cont)          //option 2
   {
      m_str1 = cont.m_str1;
   }

   public string GetMyString() { return m_str1;}       

   public void SetMyString(string str) { m_str1 = str;}

   private:

   string m_str1;
}
  • В примере весь код встроен, но в нашем реальном коде встроенный код отсутствует.

Обновление (29 сентября 2009 г.):

Некоторые из этих ответов хорошо написаны, однако, похоже, они упускают суть этого вопроса:

  • это простой надуманный пример для обсуждения использования геттеров/сеттеров и переменных.

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

  • Некоторые люди сосредоточены на строке в этом примере, однако это всего лишь пример, представьте, что это другой объект.

  • Меня не волнует производительность.мы не программируем на PDP-11

Это было полезно?

Решение

Ожидаете ли вы, как будет возвращена строка, например.пробелы обрезаны, отмечено нулевое значение и т. д.?То же самое и с SetMyString(): если ответ положительный, вам лучше использовать методы доступа, поскольку вам не нужно менять свой код в миллионе мест, а просто модифицировать эти методы получения и установки.

Другие советы

РЕДАКТИРОВАТЬ: Отвечаю на отредактированный вопрос :)

это простой надуманный пример обсудить использование геттеров/сеттеров против переменные

Если у вас есть простая коллекция переменных, которая не требует какой-либо проверки или дополнительной обработки, вы можете вместо этого рассмотреть возможность использования POD.От Часто задаваемые вопросы Страуструпа:

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

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

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

Если вы копируете переменные другого объекта, исходный объект должен находиться в допустимом состоянии.Как вообще был создан плохо сформированный исходный объект?!Разве конструкторы не должны выполнять работу по проверке?разве модифицирующие функции-члены не отвечают за поддержание инварианта класса путем проверки входных данных?Зачем вам проверять «действительный» объект в конструкторе копирования?

Меня не волнует производительность.мы не программируем на PDP-11

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


Вам следует использовать initializer list.В вашем коде m_str1 построен по умолчанию затем присвоено новое значение.Ваш код может быть примерно таким:

class Container 
{
public:
   Container() {}

   Container(const Container& cont) : m_str1(cont.m_str1)
   { }

   string GetMyString() { return m_str1;}       
   void SetMyString(string str) { m_str1 = str;}
private:
   string m_str1;
};

@cbrulak Вам не следует проверять IMO cont.m_str1 в copy constructor.Что я делаю, так это проверяю вещи в constructors.Проверка в copy constructor означает, что вы в первую очередь копируете неправильно сформированный объект, например:

Container(const string& str) : m_str1(str)
{
    if(!valid(m_str1)) // valid() is a function to check your input
    {
        // throw an exception!
    }
}

Вам следует использовать список инициализаторов, и тогда вопрос станет бессмысленным, например:

Container(const Container& rhs)
  : m_str1(rhs.m_str1)
{}

У Мэтью Уилсона есть отличный раздел. Несовершенный С++ это объясняет все о списках инициализаторов членов и о том, как вы можете использовать их в сочетании с константами и/или ссылками, чтобы сделать ваш код более безопасным.

Редактировать:пример, показывающий проверку и константу:

class Container
{
public:
  Container(const string& str)
    : m_str1(validate_string(str))
  {}
private:
  static const string& validate_string(const string& str)
  {
    if(str.empty())
    {
      throw runtime_error("invalid argument");
    }
    return str;
  }
private:
  const string m_str1;
};

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

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

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

Это дает реальное улучшение по сравнению с использованием методов установки и получения в окружающем классе.Прежде всего, когда вы помещаете их в окружающий класс, код внутри этого класса легко обходит метод получения/установки, теряя при этом выполнение всего, что должен был обеспечить установщик.Во-вторых, он поддерживает нормально выглядящие обозначения.Использование геттера и сеттера вынуждает вас писать код, который просто уродлив и труден для чтения.

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

strcpy(strcat(filename, ".ext"));

с:

filename += ".ext";

для улучшения читаемости.Но посмотрите, что произойдет, если эта строка является частью класса, который заставляет нас проходить через методы получения и установки:

some_object.setfilename(some_object.getfilename()+".ext");

Во всяком случае, код C на самом деле более читабелен, чем этот беспорядок.С другой стороны, представьте, что произойдет, если мы правильно выполним работу с общедоступным объектом класса, который определяет строку оператора и оператор =:

some_object.filename += ".ext";

Красиво, просто и читабельно, как и должно быть.Еще лучше, если нам нужно что-то применить к строке, мы можем проверить только этот небольшой класс, нам действительно нужно просмотреть только одно или два конкретных, хорошо известных места (operator=, возможно, один или два ctor для этого класса), чтобы знайте, что оно всегда применяется — это совершенно другая история, чем когда мы используем сеттер, чтобы попытаться выполнить работу.

Спросите себя, каковы затраты и выгоды.

Расходы:более высокие затраты времени выполнения.Вызов виртуальных функций в векторах — плохая идея, но сеттеры и геттеры вряд ли будут виртуальными.

Преимущества:если сеттер/геттер делает что-то сложное, вы не повторяете код;если он делает что-то неинтуитивное, вы не забудете это сделать.

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

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

В противном случае вам придется серьезно подумать.

struct alpha {
   beta* m_beta;
   alpha() : m_beta(new beta()) {}
   ~alpha() { delete m_beta; }
   alpha(const alpha& a) {
     // need to copy? or do you have a shared state? copy on write?
     m_beta = new beta(*a.m_beta);
     // wrong
     m_beta = a.m_beta;
   }

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

Конечно, может быть еще смешнее.

  • Члены, которые создаются по требованию.
  • new beta(a.beta) является неправильный на случай, если вы каким-то образом введете полиморфизм.

...винт в противном случае - пожалуйста, всегда думайте при написании конструктора копирования.

Зачем вообще нужны геттеры и сеттеры?

Просто :) - Они сохраняют инварианты - т.е.гарантирует, что ваш класс делает, например «MyString всегда имеет четное количество символов».

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

Как сказал АраК, лучше всего использовать список инициализаторов.


Не все так просто (1):
Еще одна причина использовать геттеры/сеттеры — не полагаться на детали реализации.Это странная идея для копирующего CTor: при изменении таких деталей реализации почти всегда всё равно приходится корректировать CDA.


Не все так просто (2):
Чтобы доказать мою неправоту, вы можете построить инварианты, зависящие от самого экземпляра или другого внешнего фактора.Один (очень надуманный) пример:«Если количество экземпляров ровно, длина струны равно, в противном случае это странно». В этом случае копия CTOR должна была бы бросить или отрегулировать строку.В таком случае может помочь использование сеттеров/геттеров, но это не общий случай.Не следует из странностей выводить общие правила.

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

Не говоря уже о том, что вы, вероятно, сэкономите несколько вызовов функций, если геттер не встроен.

Если ваши геттеры (встроены и) нет virtual, в их использовании нет ни плюсов, ни минусов относительно прямого доступа к членам - мне это просто кажется глупым с точки зрения стиля, но в любом случае ничего страшного.

Если ваши геттеры виртуальные, то там является над головой...но, тем не менее, именно тогда вы ДЕЙСТВИТЕЛЬНО хотите их вызвать, на случай, если они будут переопределены в подклассе!-)

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

Предположим, что установщик не только присваивает значение, но также записывает контрольную запись, регистрирует сообщение или вызывает событие.Вы хотите, чтобы это происходило для каждого свойства при копировании объекта?Вероятно, нет - поэтому вызов сеттеров в конструкторе логически неверен (даже если сеттеры на самом деле являются просто присваиваниями).

Хотя я согласен с другими авторами, что в вашем образце много «нет-нет» C++ начального уровня, отложим это в сторону и напрямую ответим на ваш вопрос:

На практике я обычно делаю многие, но не все поля* своих участников общедоступными, а затем перемещаю их для получения/установки, когда это необходимо.

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

Может быть.Но я считаю, что на практике это не всегда необходимо.Конечно, позже это причиняет боль, когда я меняю поле с общедоступного на метод получения, и иногда, когда я знаю, какое использование будет иметь класс, я даю ему команду set/get и с самого начала делаю поле защищенным или закрытым.

ЯММВ

РФ

  • вы называете поля «переменными» — я советую вам использовать этот термин только для локальных переменных внутри функции/метода.
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top