Стиль кодирования геттеров / сеттеров C ++

Некоторое время я программировал на C #, а теперь хочу освежить свои навыки в C ++.

Имея класс:

class Foo
{
    const std::string& name_;
    ...
};

Каким будет лучший подход (я хочу разрешить доступ только для чтения к полю name_):

  • используйте метод получения: inline const std::string& name() const { return name_; }
  • сделайте поле общедоступным, так как это постоянная

Спасибо.


person Alex    schedule 17.04.2009    source источник
comment
Дубликат stackoverflow.com/questions/737409/?   -  person jalf    schedule 17.04.2009
comment
Кроме того, в C ++ для переменных-членов чаще используется одинарное подчеркивание в начале.   -  person Martin Beckett    schedule 17.04.2009
comment
Мне также показалось, что завершающее подчеркивание несколько неудобно, но именно это я видел в C ++ Faq Lite.   -  person Alex    schedule 18.04.2009
comment
Ниже приведен довольно полный ответ об использовании символов подчеркивания в C ++ stackoverflow.com/questions/228783/   -  person veefu    schedule 18.04.2009
comment
@MartinBeckett Одиночные подчеркивания могут быть обычным явлением, но как слабовидящий я могу сказать вам, что их трудно читать. IDE просто любят подчеркивать вещи (ошибки, орфографию и т. Д.), А подчеркивание полностью скрывается за полезностью. Для сообщества программистов в целом, выслушайте нас, слепых, и поставьте m_ вместо _.   -  person Wes Miller    schedule 30.04.2013
comment
@WesMiller, спасибо, я никогда об этом не думал. Хотя современные IDE позволят использовать локальный стиль. Мы просто приняли завершающие символы подчеркивания (следуя некоторому стандарту), и они плохи, когда дело доходит до указателя _- ›или element_ [i]   -  person Martin Beckett    schedule 30.04.2013
comment
Ненавижу, когда люди ставят начальные символы подчеркивания или m_ в Javascript и Java. Люди, кажется, забывают, что это наименее неэлегантные решения неудобств C ++ (например, в Java поля и методы могут иметь одно и то же имя)   -  person Andy    schedule 09.06.2015
comment
@WesMiller Мне тоже трудно читать все подчеркивания, несмотря на приличное видение. Нигде я не нашел более болезненного для чтения кода, чем некоторые стандартные библиотеки C ++.   -  person Andy    schedule 09.06.2015
comment
@ Энди Это потому, что вы ищете совершенно не то место. Стандартные библиотеки не должны быть примерами красивого кода; они должны быть только рабочим кодом. И в этом суть. Авторы Stdlib должны использовать уродливые имена переменных, особенно в случае шаблонов, поскольку они должны гарантировать, что их идентификаторы не будут мешать пользователям, чтобы их код работал должным образом (если пользователь следует Стандарт). Это пример того, почему идентификаторы, содержащие двойные подчеркивания или определенные комбинации начальных подчеркиваний, зарезервированы для использования в реализации.   -  person underscore_d    schedule 24.05.2017


Ответы (8)


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

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

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

person Mr Fooz    schedule 17.04.2009
comment
Если я перехожу с функции, возвращающей std::string, на функцию, возвращающую некоторую строку Unicode, я уже нарушаю весь клиентский код. - person David Stone; 24.04.2012
comment
.. что, если бы ваше внутреннее представление было Unicode, но вы могли бы преобразовать в UTF8 для совместимости с существующими клиентами? Преобразование может выполнять метод получения, но публичное поле запрещает этот шаблон. - person JBRWilkinson; 08.05.2012
comment
@JBRWilkinson кажется мне случаем, когда мне могут понадобиться геттеры с разными именами для вариантов Unicode и UTF8. Если внутреннее представление остается прежним, но изменяется тип получателя, вы нарушаете совместимость; Указывайте на это только в том случае, если вы думаете, что внутреннее представление изменится. - person Andy; 09.06.2015
comment
@ Andy еще одна веская причина иметь геттеры для подобных вещей - наличие естественной формы по умолчанию, но также необязательных других форм. Чтобы использовать различные 8-битные кодировки, можно выбрать либо get_my_string() + get_my_utf8_string() + get_my_cp1252_string(), и т. Д., Либо get_my_string(const char* encoding = "utf8"). - person Mr Fooz; 09.06.2015
comment
@MrFooz да ... но если бы он был доступен, я бы просто вернул что-то вроде QString, который обеспечивает все эти преобразования, так что мне не нужно писать кучу геттеров для каждого поля - person Andy; 11.06.2015

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

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

class Foo {
public:
    std::string const& name() const;          // Getter
    void name(std::string const& newName);    // Setter
    ...
};

В единую общедоступную переменную-член, которая определяет operator()() для каждого:

// This class encapsulates a fancier type of name
class fancy_name {
public:
    // Getter
    std::string const& operator()() const {
        return _compute_fancy_name();    // Does some internal work
    }

    // Setter
    void operator()(std::string const& newName) {
        _set_fancy_name(newName);        // Does some internal work
    }
    ...
};

class Foo {
public:
    fancy_name name;
    ...
};

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

person j_random_hacker    schedule 17.04.2009
comment
Эй, я нашел ваш ответ перед тем, как опубликовать свой вопрос ... кажется, он почти такой же, и похоже, что вы предлагаете решение, которое я ищу, но, будучи новичком, я не могу быть уверен. Не могли бы вы взглянуть на мой вопрос здесь: stackoverflow.com/questions/8454887/ и сообщить мне, находится ли этот ответ на правильном пути к тому, чего я хотел бы достичь? - person ; 10.12.2011
comment
Если вы запутались, как я: нет operator()(). Есть operator() со списком параметров. - person mucaho; 22.08.2015
comment
Разве замена публичным членом не нарушит инкапсуляцию, позволяющую обойти геттер / сеттер? - person a1an; 19.09.2017
comment
@ a1an: Идея состоит в том, что публичный член - это не какой-то простой тип, такой как std::string (который может быть изменен вызывающим способом с нарушением инвариантности), а скорее объект некоторого типа класса (например, мой пример fancy_name class), который защищает все необходимые инварианты, открывая более ограниченный интерфейс. - person j_random_hacker; 19.09.2017

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

Что касается стиля, я согласен с другими, что вы не хотите обнажать свои интимные места. :-) Мне нравится этот паттерн для сеттеров / геттеров

class Foo
{
public:
  const string& FirstName() const;
  Foo& FirstName(const string& newFirstName);

  const string& LastName() const;
  Foo& LastName(const string& newLastName);

  const string& Title() const;
  Foo& Title(const string& newTitle);
};

Таким образом вы можете сделать что-то вроде:

Foo f;
f.FirstName("Jim").LastName("Bob").Title("Programmer");
person chrish    schedule 18.04.2009

Я думаю, что подход C ++ 11 сейчас был бы больше похож на этот.

#include <string>
#include <iostream>
#include <functional>

template<typename T>
class LambdaSetter {
public:
    LambdaSetter() :
        getter([&]() -> T { return m_value; }),
        setter([&](T value) { m_value = value; }),
        m_value()
    {}

    T operator()() { return getter(); }
    void operator()(T value) { setter(value); }

    LambdaSetter operator=(T rhs)
    {
        setter(rhs);
        return *this;
    }

    T operator=(LambdaSetter rhs)
    {
        return rhs.getter();
    }

    operator T()
    { 
        return getter();
    }


    void SetGetter(std::function<T()> func) { getter = func; }
    void SetSetter(std::function<void(T)> func) { setter = func; }

    T& GetRawData() { return m_value; }

private:
    T m_value;
    std::function<const T()> getter;
    std::function<void(T)> setter;

    template <typename TT>
    friend std::ostream & operator<<(std::ostream &os, const LambdaSetter<TT>& p);

    template <typename TT>
    friend std::istream & operator>>(std::istream &is, const LambdaSetter<TT>& p);
};

template <typename T>
std::ostream & operator<<(std::ostream &os, const LambdaSetter<T>& p)
{
    os << p.getter();
    return os;
}

template <typename TT>
std::istream & operator>>(std::istream &is, const LambdaSetter<TT>& p)
{
    TT value;
    is >> value;
    p.setter(value);
    return is;
}


class foo {
public:
    foo()
    {
        myString.SetGetter([&]() -> std::string { 
            myString.GetRawData() = "Hello";
            return myString.GetRawData();
        });
        myString2.SetSetter([&](std::string value) -> void { 
            myString2.GetRawData() = (value + "!"); 
        });
    }


    LambdaSetter<std::string> myString;
    LambdaSetter<std::string> myString2;
};

int _tmain(int argc, _TCHAR* argv[])
{
    foo f;
    std::string hi = f.myString;

    f.myString2 = "world";

    std::cout << hi << " " << f.myString2 << std::endl;

    std::cin >> f.myString2;

    std::cout << hi << " " << f.myString2 << std::endl;

    return 0;
}

Я тестировал это в Visual Studio 2013. К сожалению, чтобы использовать базовое хранилище внутри LambdaSetter, мне потребовалось предоставить общедоступный аксессор GetRawData, который может привести к нарушению инкапсуляции, но вы можете либо оставить его, либо предоставить свой собственный контейнер для хранения для T или просто убедитесь, что вы используете "GetRawData" только тогда, когда пишете собственный метод получения / установки.

person Lambage    schedule 09.12.2013
comment
разве это не излишество? в этом подходе используется хорошая инкапсуляция, поэтому вам нужно поддерживать только класс LambdaSetter, но с точки зрения производительности, я думаю, было бы неплохо избавиться от указателей на функции? А также myString.SetGetter( выглядит немного неудобным, если дать объекту функцию, которая может выполнять данное действие, а именно установку myString. Подход определенно идет в правильном направлении, но я бы упростил его, не имея std::functions - person Gabriel; 26.04.2015
comment
Конечно, это излишество, но то, что он дает вам, - это возможность во время выполнения изменять способ установки / получения переменной. Избавление от std :: functions превратит его обратно в set / get, определяемый во время компиляции (вероятно, подходит для большинства приложений). Шаблонный класс мог бы лучше служить для этого, поэтому класс при желании мог бы использовать метод set / get и это будет определено во время компиляции. - person Lambage; 03.06.2015
comment
Я бы назвал это преждевременной оптимизацией. - person BJovke; 30.06.2018
comment
В стороне, я бы никогда не стал этим заниматься, пытаться достичь того, что C # делает изначально в C ++, - глупая затея, но она действительно отвечает на вопрос OP. - person Lambage; 12.07.2018

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

class Foo {
    public:
        const std::string& getName() const {return name_;}
    private:
        const std::string& name_;
};

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

person Dan Breslau    schedule 17.04.2009
comment
Итак, позвольте мне резюмировать этот ответ: 1) маловероятно, что вы когда-нибудь измените реализацию getName(), потому что ... черт возьми, данные просто хранятся, как в мире вы будете пересчитывать их каждый раз, когда к ним обращаются? Вы используете C ++ для повышения производительности, не так ли? и 2) в конце концов, даже если вы действительно придумаете такой сумасшедший вариант использования, вы на самом деле не сможете этого сделать, потому что невозможно вернуть const ref без запуска неопределенного поведения. Вы еще не уверены? - person ulidtko; 19.03.2012
comment
ulidtko, судя по тону вашего голоса, я сомневаюсь, что любой ответ, который я вам дам, мог бы вас успокоить. Однако позвольте мне указать на одну вещь: я написал . Обратите внимание, что если вы измените getName () так, чтобы он возвращал вычисленное значение, он не смог бы вернуть const ref. Это нормально, потому что это не потребует никаких изменений в вызывающих (по модулю перекомпиляции). Другими словами, вам придется изменить метод с const std::string& getName() const на std::string getName() const. Конечно, изменение встроенной функции или поля всегда требует перекомпиляции, поэтому это не увеличивает нагрузку на время компиляции. - person Dan Breslau; 20.03.2012
comment
ваше примечание здесь правильное. Возможно, вам также пригодятся некоторые умные указатели. Однако это правда, что у меня есть мнение о геттерах и сеттерах в C ++, и я собираю аргументы за и против этого. - person ulidtko; 20.03.2012
comment
Относительно этого аргумента для геттеров (т.е. возможности переключиться на пересчет значения вместо того, чтобы возвращать его сохраненным), я могу сделать следующее возражение: почти наверняка, пересчитывая значение в геттере, вы нарушите контракт. Большинство клиентов будут полагаться на определенные интуитивно понятные свойства геттеров: они быстрые, они не бросают, не выделяют, должны быть потокобезопасными и т. Д. Вам может повезти, и все по-прежнему будет работать с вашим нетривиальным геттером. , но программирование - это не про удачу. - person ulidtko; 20.03.2012
comment
Я согласен с вами, что иногда вызывающий объект может полагаться на геттер, который работает практически так же быстро, как доступ к полю. В некоторых кругах это используется как аргумент против языковой поддержки прозрачных геттеров. Я симпатизирую идее неявного контракта производительности, но я думаю, что здесь применимо и небольшое предостережение: если разработчик использует средства доступа, они оставляют за собой право изменять реализацию. И если они явно не обещали постоянный доступ, вам не следует писать код, предполагающий постоянный доступ. - person Dan Breslau; 21.03.2012

Избегайте общедоступных переменных, за исключением классов, которые по сути являются структурами в стиле C. Это просто не лучшая практика.

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

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

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

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

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

person David Thornley    schedule 17.04.2009
comment
Я склонен не соглашаться, потому что с публичным полем ситуация с измененными требованиями не так фатальна: вы все равно можете использовать некоторые шаблоны, наследование и operator= для имитации свойства на C ++. И на самом деле большую часть времени вам это не понадобится (серьезно, сколько раз вы использовали нетривиальные геттеры или сеттеры?), В то время как клиентский синтаксис _2 _ / _ 3_ просто уродлив. - person ulidtko; 19.03.2012

Собрал идеи из нескольких источников C ++ и воплотил их в красивый, но довольно простой пример для геттеров / сеттеров на C ++:

class Canvas { public:
    void resize() {
        cout << "resize to " << width << " " << height << endl;
    }

    Canvas(int w, int h) : width(*this), height(*this) {
        cout << "new canvas " << w << " " << h << endl;
        width.value = w;
        height.value = h;
    }

    class Width { public:
        Canvas& canvas;
        int value;
        Width(Canvas& canvas): canvas(canvas) {}
        int & operator = (const int &i) {
            value = i;
            canvas.resize();
            return value;
        }
        operator int () const {
            return value;
        }
    } width;

    class Height { public:
        Canvas& canvas;
        int value;
        Height(Canvas& canvas): canvas(canvas) {}
        int & operator = (const int &i) {
            value = i;
            canvas.resize();
            return value;
        }
        operator int () const {
            return value;
        }
    } height;
};

int main() {
    Canvas canvas(256, 256);
    canvas.width = 128;
    canvas.height = 64;
}

Вывод:

new canvas 256 256
resize to 128 256
resize to 128 64

Вы можете протестировать его онлайн здесь: http://codepad.org/zosxqjTX

PS: FO Yvette ‹3

person kungfooman    schedule 25.10.2018

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

С уважением,

person Abhay    schedule 17.04.2009
comment
инкапсулируйте то, что меняется - перевод: пишите геттеры и сеттеры. Такая аргументация меня не купит. В то время как проблема переключения внутреннего представления без нарушения клиентского кода решается с помощью свойств (которые можно эмулировать в C ++), синтаксис методов получения и установки IMO действительно уродлив и заставляет всех клиентов писать уродливый код. Поэтому мне лучше сделать переменную-член общедоступной, особенно если она const. - person ulidtko; 19.03.2012