Подтвердить что ты не робот

Сравнение между постоянными аксессуарами частных членов

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

class MyClassReference
{
private:
    int myPrivateInteger;

public:
    const int & myIntegerAccessor;

    // Assign myPrivateInteger to the constant accessor.
    MyClassReference() : myIntegerAccessor(myPrivateInteger) {}
};

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

class MyClassGetter
{
private:
    int myPrivateInteger;

public:
    int getMyInteger() const { return myPrivateInteger; }
};

Необходимость (или ее отсутствие) для "геттеров/сеттеров" уже неоднократно хешировалась по таким вопросам, как: Соглашения для методов доступа (getters and setters) в С++ Однако это не проблема.

Оба этих метода имеют ту же функциональность, что и синтаксис:

MyClassGetter a;
MyClassReference b;    
int SomeValue = 5;

int A_i = a.getMyInteger(); // Allowed.    
a.getMyInteger() = SomeValue; // Not allowed.

int B_i = b.myIntegerAccessor; // Allowed.    
b.myIntegerAccessor = SomeValue; // Not allowed.

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

  • Я даже не думал об этом, но использую метод "getter" , поскольку это "Установленная практика".
  • Они работают одинаково (оба они работают с одинаковой эффективностью), но используют метод "getter" , поскольку это "Установленная практика".

Хотя оба эти ответа были разумными, поскольку оба они не смогли объяснить "почему", я остался неудовлетворенным и решил продолжить рассмотрение этого вопроса. В то время как я провел несколько тестов, таких как среднее использование символов (они примерно одинаковы), среднее время набора текста (опять-таки примерно такое же), один тест показал крайнее несоответствие между этими двумя методами. Это был тест времени выполнения для вызова аксессуар и назначение его целому числу. Без каких-либо флагов -OX (в режиме отладки) MyClassReference выполняется примерно на 15% быстрее. Однако, как только был добавлен флаг -OX, в дополнение к выполнению намного быстрее оба метода работают с одинаковой эффективностью.

Таким образом, мой вопрос состоит из двух частей.

  • Как эти два метода отличаются друг от друга, и что заставляет вас быть быстрее/медленнее, чем другие только с определенными флагами оптимизации?
  • Почему существует установленная практика использования постоянной функции "getter" , а использование постоянной ссылки редко известно, а не используется?

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

4b9b3361

Ответ 1

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

Скопировать семантику

A живой пример:

#include <iostream>

class Broken {
public:
    Broken(int i): read_only(read_write), read_write(i) {}

    int const& read_only;

    void set(int i) { read_write = i; }

private:
    int read_write;
};

int main() {
    Broken original(5);
    Broken copy(original);

    std::cout << copy.read_only << "\n";

    original.set(42);

    std::cout << copy.read_only << "\n";
    return 0;
}

Урожайность:

5
42

Проблема заключается в том, что при выполнении копирования copy.read_only указывает на original.read_write. Это может привести к зависанию ссылок (и сбоев).

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

Назначение разбито

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

int main() {
    Broken original(5);
    Broken copy(4);
    copy = original;

    std::cout << copy.read_only << "\n";

    original.set(42);

    std::cout << copy.read_only << "\n";
    return 0;
}

генерирует ошибку:

prog.cpp: In function 'int main()':
prog.cpp:18:7: error: use of deleted function 'Broken& Broken::operator=(const Broken&)'
  copy = original;
       ^
prog.cpp:3:7: note: 'Broken& Broken::operator=(const Broken&)' is implicitly deleted because the default definition would be ill-formed:
 class Broken {
       ^
prog.cpp:3:7: error: non-static reference member 'const int& Broken::read_only', can't use default assignment operator

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

Если вы не исправите его, Broken можно использовать только очень ограниченным образом; вы, возможно, никогда не сможете разместить его внутри std::vector.

Увеличенная связь

Отказ от ссылки на ваши внутренние детали увеличивает сцепление. Утечка детали реализации (тот факт, что вы используете int, а не short, long или long long).

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

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


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

Увеличенный размер объекта

В то время как иногда ссылки могут быть устранены, это вряд ли когда-либо произойдет здесь. Это означает, что каждый ссылочный элемент будет увеличивать размер объекта по меньшей мере на sizeof(void*), а также потенциально наложение для выравнивания.

Исходный класс MyClassA имеет размер 4 на платформах x86 или x86-64 с компиляторами основного потока.

Класс Broken имеет размер 8 на x86 и 16 на платформах x86-64 (последний из-за дополнения, поскольку указатели выравниваются на границах 8 байтов).

Увеличенный размер может привести к перегрузке кэшей CPU, при этом из-за большого количества элементов вы можете быстро получить замедление (из-за этого нелегко будет иметь векторы Broken из-за его разбитого оператора присваивания).

Лучшая производительность при отладке

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

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


В конце используйте геттер. Он установил соглашение по ряду причин:)

Ответ 2

Ответ на вопрос № 2 заключается в том, что иногда вы можете изменить внутренние элементы класса. Если вы сделали все свои атрибуты общедоступными, они являются частью интерфейса, поэтому даже если вы придумали лучшую реализацию, которая им не нужна (скажем, она может быстро пересчитать значение "на лету" и побрить размер каждого экземпляр, поэтому программы, из которых 100 миллионов из них теперь используют на 400-800 МБ меньше памяти), вы не можете удалить его, не нарушая зависимый код.

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

Ответ 3

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

Кстати, метод getter также может быть виртуальным.

Ответ 4

Чтобы ответить на вопрос 2:

const_cast<int&>(mcb.myIntegerAccessor) = 4;

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