Как удалить дублирование кода между похожими константными и неконстантными функциями-членами?

Допустим, у меня есть следующий class X, где я хочу вернуть доступ внутреннему члену:

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    Z& Z(size_t index)
    {
        // massive amounts of code for validating index

        Z& ret = vecZ[index];

        // even more code for determining that the Z instance
        // at index is *exactly* the right sort of Z (a process
        // which involves calculating leap years in which
        // religious holidays fall on Tuesdays for
        // the next thousand years or so)

        return ret;
    }
    const Z& Z(size_t index) const
    {
        // identical to non-const X::Z(), except printed in
        // a lighter shade of gray since
        // we're running low on toner by this point
    }
};

Две функции-члены X::Z() и X::Z() const имеют идентичный код внутри фигурных скобок. Это повторяющийся код и может вызвать проблемы с обслуживанием длинных функций со сложной логикой.

Есть ли способ избежать дублирования кода?


person Kevin    schedule 23.09.2008    source источник
comment
В этом примере я бы вернул значение в случае const, поэтому вы не сможете выполнить рефакторинг ниже. int Z () const {return z; }   -  person Matt Price    schedule 24.09.2008
comment
Что касается основных типов, вы абсолютно правы! Мой первый пример был не очень хорош. Допустим, вместо этого мы возвращаем какой-то экземпляр класса. (Я обновил вопрос, чтобы отразить это.)   -  person Kevin    schedule 24.09.2008


Ответы (20)


Подробное объяснение см. В разделе «Избегайте дублирования в const и не const функциях-членах» на стр. 23, в п. 3 По возможности используйте const в Effective C ++, 3d ed Скотта Мейерса, ISBN-13: 9780321334879.

alt text

Вот решение Мейерса (упрощенное):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

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

person jwfearn    schedule 23.09.2008
comment
Никого не уволили за то, что он следил за Скоттом Мейерсом :-) - person Steve Jessop; 24.09.2008
comment
const_cast почти никогда не следует использовать. Это нарушает постоянство. Это особенно актуально для встраиваемых систем, где у вас есть ПЗУ. В общем, использовать const_cast - плохая идея. - person Ted; 24.09.2008
comment
Ой ... у вас уже могут быть проблемы, если вы рассчитываете на const == ROM. Компиляторы недостаточно хороши, чтобы доказать такое сильное утверждение. - person Adam Mitz; 24.09.2008
comment
witkamp правильно, что вообще использовать const_cast плохо. Как объясняет Мейерс, это конкретный случай, когда это не так. @Adam: ROM = ›const в порядке. const == ROM - это, очевидно, ерунда, поскольку любой может волей-неволей преобразовать неконстантный в const: это эквивалентно простому выбору не изменять что-либо. - person Steve Jessop; 24.09.2008
comment
В общем, я бы предложил использовать const_cast вместо static_cast для добавления const, поскольку это предотвращает случайное изменение типа. - person Greg Rogers; 23.11.2008
comment
@witkamp Вы упускаете суть, есть два вида константности: физическая и логическая. - person mloskot; 15.11.2012
comment
Что, если ваш const char & get() const возвращает что-то, что на самом деле является объявленным const? Тогда ваша неконстантная версия get будет эффективно преобразовывать const_cast что-то, что никогда не должно было быть доступно для записи. - person HelloGoodbye; 28.03.2013
comment
@HelloGoodbye: я думаю, что Майерс предполагает минимум интеллекта от дизайнера интерфейса класса. Если get()const возвращает что-то, что было определено как константный объект, тогда вообще не должно быть неконстантной версии get(). На самом деле, мое мнение об этом изменилось со временем: решение на основе шаблона - единственный способ избежать дублирования и проверить корректность констант, проверенную компилятором, поэтому лично я бы больше не использовал const_cast, чтобы избежать дублирования кода, я бы выбрал между помещением дублированного кода в шаблон функции или оставлением его дублированным. - person Steve Jessop; 05.04.2013
comment
Следующие два шаблона значительно улучшают читаемость этого решения: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); } и template<typename T> T& variable(const T& _) { return const_cast<T&>(_); }. Тогда вы можете: return variable(constant(*this).get()); - person Casey Rodarmor; 21.07.2014
comment
Конечно, абсолютно равноценно, но я считаю этот вариант чуть более эстетичным: return const_cast<char &>(const_cast<const C *>(this)->get()); - person Aconcagua; 26.06.2017
comment
@GregRogers re: В общем, я бы предложил использовать const_cast вместо static_cast для добавления const, поскольку это предотвращает случайное изменение типа. Это неправильно, поскольку const_cast предназначен для отбрасывания константы. Для преобразования в константу следует использовать static_cast. Это дословно взято из книги Скотта Мейерса. - person xdavidliu; 20.10.2020
comment
Обратите внимание, что это будет работать только с двумя функциями-членами, которые возвращают точно такой же тип (т.е. не будет работать, если одна возвращает char &, а другая возвращает char). Спустился в ужасную кроличью нору, пытаясь понять, почему это не работает. - person Danny; 23.06.2021

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

class X
{
   std::vector<Z> vecZ;

public:
   const Z& z(size_t index) const
   {
      // same really-really-really long access 
      // and checking code as in OP
      // ...
      return vecZ[index];
   }

   Z& z(size_t index)
   {
      // One line. One ugly, ugly line - but just one line!
      return const_cast<Z&>( static_cast<const X&>(*this).z(index) );
   }

 #if 0 // A slightly less-ugly version
   Z& Z(size_t index)
   {
      // Two lines -- one cast. This is slightly less ugly but takes an extra line.
      const X& constMe = *this;
      return const_cast<Z&>( constMe.z(index) );
   }
 #endif
};

ПРИМЕЧАНИЕ. Важно, чтобы вы НЕ помещали логику в неконстантную функцию и чтобы константная функция вызывала неконстантную функцию - это может привести к неопределенное поведение. Причина в том, что экземпляр константного класса приводится к непостоянному экземпляру. Неконстантная функция-член может случайно изменить класс, что в соответствии со стандартом C ++ приведет к неопределенному поведению.

person Community    schedule 23.09.2008
comment
Вау ... это ужасно. Вы просто увеличили объем кода, уменьшили ясность и добавили две stinkin 'const_cast ‹› s. Возможно, у вас есть пример, когда это действительно имеет смысл? - person Shog9; 24.09.2008
comment
Да, этот ужасный зверь намного хуже, чем дублирование кода. - person 17 of 26; 24.09.2008
comment
Хорошо, вы изменили его, чтобы работать с чем-то другим, кроме простого целого числа. Дублирующий код все еще более понятен, чем недублирующий код. - person Shog9; 24.09.2008
comment
Эй, не говорите об этом !, это может быть некрасиво, но, по словам Скотта Мейерса, это (почти) правильный путь. См. Эффективный C ++, 3-е изд., Элемент 3 под заголовком Избегание дублирования в константных и не связанных с затратами функциях-членах. - person jwfearn; 24.09.2008
comment
Хотя я понимаю, что решение может быть некрасивым, представьте, что код, определяющий, что возвращать, имеет длину 50 строк. В таком случае дублирование крайне нежелательно, особенно когда вам нужно повторно разложить код. Я сталкивался с этим много раз в своей карьере. - person Kevin; 24.09.2008
comment
@jwfearn - Из любопытства, что не так? И чем мы со Скоттом Мейерсом различаемся? - person Kevin; 24.09.2008
comment
Кевин - вам нужно как минимум редактирование №6 - в уродливом наборе приведений у вас есть const_cast ‹int &›, который должен быть const_cast ‹Z &› - person Michael Burr; 24.09.2008
comment
Разница между this и Meyers в том, что у Meyers есть static_cast ‹const X &› (* this). const_cast предназначен для удаления const, а не для его добавления. - person Steve Jessop; 24.09.2008
comment
Согласно стандарту C ++ недопустимо использовать const_cast для удаления квалификатора const из объекта, который изначально был создан как const. Насколько я знаю, этот код вызывает неопределенное поведение. - person Violet Giraffe; 04.07.2016
comment
@VioletGiraffe мы знаем, что объект изначально не был создан как const, поскольку он не является членом константного объекта неконстантного объекта, что мы знаем, потому что мы находимся в неконстантном методе указанного объекта. Компилятор не делает этого вывода, он следует консервативному правилу. Как вы думаете, почему существует const_cast, если не в такой ситуации? - person Caleth; 15.03.2017

C ++ 17 обновил лучший ответ на этот вопрос:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

Это имеет следующие преимущества:

  • Очевидно, что происходит
  • Имеет минимальные накладные расходы на код - помещается в одну строку
  • Трудно ошибиться (можно только случайно выбросить volatile, но volatile - редкий квалификатор)

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

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
constexpr T * as_mutable(T const * value) noexcept {
    return const_cast<T *>(value);
}
template<typename T>
constexpr T * as_mutable(T * value) noexcept {
    return value;
}
template<typename T>
void as_mutable(T const &&) = delete;

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

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}
person David Stone    schedule 18.11.2017
comment
Обратите внимание, что as_mutable с удаленной перегрузкой const rvalue (что обычно предпочтительнее) предотвращает работу последнего примера, если f() возвращает T вместо T&. - person Max Truxa; 25.07.2018
comment
@MaxTruxa: Да, и это хорошо. Если бы он просто скомпилировался, у нас была бы свисающая ссылка. В случае, когда f() возвращает T, мы не хотим иметь две перегрузки, достаточно только версии const. - person David Stone; 26.07.2018
comment
Совершенно верно, я прошу прощения за свой вчерашний мозговой пердун, понятия не имею, о чем я думал, когда писал этот комментарий. Я смотрел на пару констант / изменяемых геттеров, возвращающих shared_ptr. Так что мне действительно нужно было что-то вроде as_mutable_ptr, которое выглядит почти идентично as_mutable выше, за исключением того, что оно принимает и возвращает shared_ptr и использует std::const_pointer_cast вместо const_cast. - person Max Truxa; 26.07.2018
comment
Если метод возвращает T const*, тогда это будет привязано к T const* const&&, а не к T const* const& (по крайней мере, в моем тестировании так и было). Мне пришлось добавить перегрузку для T const* в качестве типа аргумента для методов, возвращающих указатель. - person monkey0506; 12.08.2019
comment
При дальнейшем размышлении о причинах удаления перегрузки T&& в первую очередь я решил просто использовать отдельный метод. Хотя приятно думать, что компилятор улавливает любые ошибки при возврате висящей ссылки, я думаю, что лучше просто быть немного более подробным там, где это необходимо. - person monkey0506; 08.09.2019
comment
@ monkey0506: Я обновил свой ответ, добавив в него указатели и ссылки. - person David Stone; 10.09.2019
comment
Вы упомянули, что ваше первое решение (с использованием std::as_const) может случайно избавиться от volatile. Вышеупомянутый ответ (Скотт Мейер) также страдает от этой проблемы? - person user2023370; 17.04.2020
comment
Теоретически такой риск несет любое решение, использующее const_cast. Помещение const_cast в одну функцию, которая определяет точный тип и использует этот выведенный тип в качестве аргумента для const_cast, делает это невозможным (если в моей реализации as_mutable нет ошибки). Однако, учитывая редкость volatile, это, вероятно, не является серьезной проблемой. - person David Stone; 18.04.2020

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

template <typename T>
struct NonConst {typedef T type;};
template <typename T>
struct NonConst<T const> {typedef T type;}; //by value
template <typename T>
struct NonConst<T const&> {typedef T& type;}; //by reference
template <typename T>
struct NonConst<T const*> {typedef T* type;}; //by pointer
template <typename T>
struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference

template<typename TConstReturn, class TObj, typename... TArgs>
typename NonConst<TConstReturn>::type likeConstVersion(
   TObj const* obj,
   TConstReturn (TObj::* memFun)(TArgs...) const,
   TArgs&&... args) {
      return const_cast<typename NonConst<TConstReturn>::type>(
         (obj->*memFun)(std::forward<TArgs>(args)...));
}

Эту вспомогательную функцию можно использовать следующим образом.

struct T {
   int arr[100];

   int const& getElement(size_t i) const{
      return arr[i];
   }

   int& getElement(size_t i) {
      return likeConstVersion(this, &T::getElement, i);
   }
};

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

person Pait    schedule 27.05.2013
comment
Жаль, что у нас нет std::remove_bottom_const, чтобы пойти с std::remove_const. - person TBBle; 04.02.2016
comment
Мне не нравится это решение, потому что оно по-прежнему включает const_cast. Вы можете сделать getElement сам шаблон и использовать черту типа внутри mpl::conditional типов, которые вам нужны, например iterators или constiterators, если необходимо. Настоящая проблема в том, как сгенерировать константную версию метода, если эта часть подписи не может быть шаблонизирована? - person v.oddou; 04.03.2016
comment
@ v.oddou: std::remove_const<int const&> is int const & (убрать квалификацию верхнего уровня const), отсюда и гимнастика NonConst<T> в этом ответе. Предполагаемый std::remove_bottom_const может удалить квалификацию const нижнего уровня и сделать в точности то, что NonConst<T> делает здесь: std::remove_bottom_const<int const&>::type = ›int&. - person TBBle; 04.03.2016
comment
@ v.oddou: Я думаю, вы не можете обойтись без двух отдельных объявлений по той причине, которую вы упомянули. - person Pait; 04.03.2016
comment
И никто не должен использовать это решение в производственном ПО. Это слишком сложно. Либо продублируйте свой код, если он достаточно простой, либо используйте решение Скотта Мейера, потому что это уродливая, но хорошо известная идиома. - person Pait; 04.03.2016
comment
Это решение не работает, если getElement перегружен. Тогда указатель на функцию не может быть разрешен без явного указания параметров шаблона. Почему? - person John; 20.06.2016
comment
Я согласен с Джоном. Чтобы получить поддержку, вот тест: ideone.com/fwqmsB. В настоящее время он компилируется, но если раскомментировать / ** / (функция перегрузки), он больше не будет компилироваться. Было бы неплохо, если бы это решение было немного улучшено. - person javaLover; 22.03.2017
comment
@John Это проблема с разрешением перегрузки C ++ для указателей функций-членов (см. Также stackoverflow.com/questions/2942426/). В настоящее время это может быть решено только на стороне вызывающего абонента. Однако указывать все параметры шаблона необязательно. Только первый TConstReturn тоже работает. Как 2_ - person Pait; 11.04.2017
comment
Вам необходимо исправить ответ, чтобы использовать идеальную пересылку C ++ 11: likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); } Complete: gist.github.com/ BlueSolei / bca26a8590265492e2f2760d3cefcf83 - person ShaulF; 18.05.2017

Хороший вопрос и хорошие ответы. У меня есть другое решение, которое не использует приведения:

class X {

private:

    std::vector<Z> v;

    template<typename InstanceType>
    static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) {
        // massive amounts of code for validating index
        // the instance variable has to be used to access class members
        return instance.v[i];
    }

public:

    const Z& get(std::size_t i) const {
        return get(*this, i);
    }

    Z& get(std::size_t i) {
        return get(*this, i);
    }

};

Однако у него есть уродство, заключающееся в том, что он требует статического члена и необходимости использовать внутри него переменную instance.

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

person gd1    schedule 08.12.2013
comment
Что ж, давайте остановимся на том простом факте, что вы добавили больше шаблонов. Во всяком случае, это следует использовать как пример того, почему языку нужен способ изменения квалификаторов функций вместе с возвращаемым типом auto get(std::size_t i) -> auto(const), auto(&&). Почему '&&'? Ах, так что я могу сказать: auto foo() -> auto(const), auto(&&) = delete; - person kfsone; 28.04.2015
comment
gd1: именно то, что я имел в виду. @kfsone и то, что я тоже сделал. - person v.oddou; 04.03.2016
comment
@kfsone синтаксис должен включать ключевое слово this. Я предлагаю template< typename T > auto myfunction(T this, t args) -> decltype(ident) Ключевое слово this будет распознаваться как неявный аргумент экземпляра объекта и позволить компилятору распознать, что myfunction является членом или T. T будет автоматически выведен на сайте вызова, который всегда будет типом класса, но с бесплатной квалификацией cv. - person v.oddou; 04.03.2016
comment
Это решение соответствует требованиям stackoverflow.com/a/38751554/259543. То есть статическая функция-член для интерполяции квалификаторов *this с использованием универсальных ссылок. - person alecov; 11.08.2016
comment
Это решение также имеет преимущество (по сравнению с решением const_cast), позволяющее возвращать iterator и const_iterator. - person Jarod42; 08.08.2017
comment
Если реализация перемещена в файл cpp (и поскольку метод, чтобы не дублировать его, не должен быть тривиальным, вероятно, так оно и есть), static может выполняться в области видимости файла, а не в области класса. :-) - person Jarod42; 08.08.2017
comment
@ Jarod42 ммм частные участники? - person gd1; 08.08.2017
comment
@ gd1: Вы все еще можете передать их в дополнение / замену *this, если это необходимо. - person Jarod42; 08.08.2017
comment
@ Jarod42 немного некрасиво - person gd1; 08.08.2017
comment
return get_impl(v, i); vs return get_impl(*this, i); не IMO, но return get_impl(*this, v, member1, member2, i); будет, и в этом случае статический член шаблона все еще доступен (и может быть реализован в файле cpp (с соответствующими геттерами), который используется только в двух местах). - person Jarod42; 08.08.2017
comment
Это лучший способ, который я знаю, определенно должен быть принятый ответ. Сам набирал, а потом уже нашел вот эту. Вот пример C ++ 17 с добавленным дополнительным тестом SFINAE: godbolt.org/z/mMK4r3 - person atablash; 05.12.2019
comment
Мне больше всего нравится это решение. Это позволяет избежать многих скрытых ошибок. Сообразительность может обеспечить вам 99% -ную безопасность с константным приведением, но есть несколько крайних случаев, скрывающихся в тени. - person iPherian; 16.01.2020
comment
Это решение, которое следует использовать (+/- регулировки), если у вас есть побочные эффекты, кстати. Как, например, при реализации нединамического шаблона посетителя, когда посетитель может делать разные вещи в зависимости от константности объекта (читать, когда константа, писать, когда не константа). - person Victor Drouin; 07.12.2020
comment
Отличное решение позволяет избежать приведения типов, которые могут быть введены неверно, что приведет к ошибке, и избежать макросов, загрязняющих глобальное пространство имен. Я не считаю это более обычным, чем другие ответы, и вряд ли вызовет ошибку. Было бы здорово, если бы был другой способ установить тип возвращаемого значения без использования decltype для члена экземпляра. - person Medran; 04.06.2021
comment
@Medran Спасибо. Этот ответ был написан в 2013 году: подробный завершающий decltype, вероятно, больше не требуется, decltype (auto), вероятно, сделает это. - person gd1; 05.06.2021

Немного более подробный, чем Мейерс, но я могу сделать следующее:

class X {

    private:

    // This method MUST NOT be called except from boilerplate accessors.
    Z &_getZ(size_t index) const {
        return something;
    }

    // boilerplate accessors
    public:
    Z &getZ(size_t index)             { return _getZ(index); }
    const Z &getZ(size_t index) const { return _getZ(index); }
};

У частного метода есть нежелательное свойство, заключающееся в том, что он возвращает неконстантный Z & для константного экземпляра, поэтому он частный. Частные методы могут нарушать инварианты внешнего интерфейса (в этом случае желаемый инвариант - это «константный объект не может быть изменен через полученные через него ссылки на объекты, которые он имеет-a»).

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

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

[Изменить: Кевин правильно указал, что _getZ может захотеть вызвать другой метод (скажем, generateZ), который специализируется на константе так же, как и getZ. В этом случае _getZ увидит const Z & и должен будет преобразовать его в const_cast перед возвратом. Это по-прежнему безопасно, поскольку шаблонный аксессуар контролирует все, но не совсем очевидно, что это безопасно. Кроме того, если вы сделаете это, а затем измените generateZ, чтобы он всегда возвращал const, вам также нужно изменить getZ, чтобы всегда возвращать const, но компилятор не скажет вам, что вы это делаете.

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

person Steve Jessop    schedule 23.09.2008
comment
По-прежнему существует проблема, заключающаяся в том, что возвращаемая вами вещь может быть константой для константного экземпляра X. В этом случае вам по-прежнему требуется const_cast в _getZ (...). При неправильном использовании более поздними разработчиками он все равно может привести к UB. Если возвращаемый объект является «изменяемым», то это хорошее решение. - person Kevin; 24.09.2008
comment
Когда я сказал, что '_getZ (...)' может использоваться неправильно, я имел в виду, что если будущий разработчик не понял, что он должен использоваться только общедоступными функциями, и вызвал его напрямую, но изменил значение в постоянный экземпляр X, то это может привести к UB. Это возможно, если не задокументировано. - person Kevin; 24.09.2008
comment
Любая закрытая функция (черт возьми, общедоступная тоже) может быть неправильно использована более поздними разработчиками, если они решат игнорировать инструкции BLOCK CAPITAL при ее правильном использовании в файле заголовка, а также в Doxygen и т. Д. Я не могу это остановить, и я не считаю это своей проблемой, так как инструкции просты для понимания. - person Steve Jessop; 24.09.2008
comment
-1: Это не работает во многих ситуациях. Что, если something в функции _getZ() является переменной экземпляра? Компилятор (или, по крайней мере, некоторые компиляторы) будут жаловаться, что, поскольку _getZ() является константой, любая переменная экземпляра, на которую есть ссылка внутри, также является константой. Таким образом, something будет константным (это будет тип const Z&) и его нельзя будет преобразовать в Z&. По моему (правда, несколько ограниченному) опыту, большую часть времени something является переменной экземпляра в подобных случаях. - person Gravity; 05.08.2011
comment
@GravityBringer: тогда что-то должно включать const_cast. Он был задуман как заполнитель для кода, необходимого для получения неконстантного возврата из объекта const, а не как заполнитель для того, что было бы в дублированном геттере. Так что что-то не просто переменная экземпляра. - person Steve Jessop; 05.08.2011
comment
Понятно. Однако это действительно снижает полезность техники. Я бы убрал отрицательный голос, но ТАК не позволит мне. - person Gravity; 07.08.2011
comment
Таким образом достигается то же, что и у Мейерса, только ваш const_cast скрыт в something. Если something не должен был быть const_casted, это было бы потому, что его нельзя было использовать для изменения объекта, для которого был вызван метод, и тогда вам не понадобилась бы какая-либо неконстантная версия метода. - person HelloGoodbye; 04.04.2013
comment
@HelloGoodbye: это не так. Неверно, что отдельные константные и неконстантные версии нужны только в том случае, если требуется const_cast. Например, something может сводиться к *some_pointer_data_member (и предположим, что вы хотите вернуть ссылку на константу в случае константы), и в этом случае код Мейерса вводит приведение, но на самом деле он вам не нужен. Его код представляет собой совершенно общий шаблон, но большинство актуальных функций, которые вы пишете, более конкретны :-) - person Steve Jessop; 05.04.2013
comment
Что ж, я думаю, в этом случае вы можете изменить свой собственный экземпляр класса даже из метода const, который вызывается в экземпляре, по крайней мере, если член-указатель указывает на член в экземпляре или на сам экземпляр, поскольку компилятор не знать, к чему обращается указатель. Меня это не поразило до сих пор. - person HelloGoodbye; 05.04.2013
comment
Это очень концептуальный ответ, он имеет большой смысл, когда возвращаемая ссылка является указателем класса. - person alfC; 19.10.2015
comment
@HelloGoodbye, где скрытый const_cast? Если возвращенная вещь не является константой, код совершенно правильный. Смысл этого ответа в том, что он не использует const_cast явное или явное. Он достигает эффекта, просто нарушая обычное соглашение, но делает это в своей частной реализации. - person alfC; 19.10.2015

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

.h файл:

#include <vector>

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    const std::vector<Z>& GetVector() const { return vecZ; }
    std::vector<Z>& GetVector() { return vecZ; }

    Z& GetZ( size_t index );
    const Z& GetZ( size_t index ) const;
};

.cpp файл:

#include "constnonconst.h"

template< class ParentPtr, class Child >
Child& GetZImpl( ParentPtr parent, size_t index )
{
    // ... massive amounts of code ...

    // Note you may only use methods of X here that are
    // available in both const and non-const varieties.

    Child& ret = parent->GetVector()[index];

    // ... even more code ...

    return ret;
}

Z& X::GetZ( size_t index )
{
    return GetZImpl< X*, Z >( this, index );
}

const Z& X::GetZ( size_t index ) const
{
    return GetZImpl< const X*, const Z >( this, index );
}

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

[Edit: удалено ненужное включение cstdio, добавленное во время тестирования.]

person Andy Balaam    schedule 13.01.2009
comment
Вы всегда можете сделать сложную функцию реализации статическим членом, чтобы получить доступ к закрытым членам. Функция должна быть объявлена ​​только в файле заголовка класса, определение может находиться в файле реализации класса. В конце концов, это часть реализации класса. - person CB Bailey; 13.01.2009
comment
Ааа да хорошая идея! Мне не нравятся элементы шаблона, появляющиеся в заголовке, но если, поскольку здесь это потенциально значительно упрощает реализацию, это, вероятно, того стоит. - person Andy Balaam; 14.01.2009
comment
+1 к этому решению, которое не дублирует никакого кода и не использует какой-либо уродливый const_cast (который может случайно использоваться, чтобы помешать чему-то, что на самом деле должно быть константным, по отношению к чему-то, что не является). - person HelloGoodbye; 05.04.2013
comment
В настоящее время это можно упростить с помощью выведенного типа возвращаемого значения для шаблона (особенно полезно, поскольку это уменьшает количество дублируемых элементов в классе в случае члена). - person Davis Herring; 19.03.2020

Для тех (как я), кто

  • используйте c ++ 17
  • хотите добавить наименьшее количество шаблонов / повторений и
  • не против использовать макросы (пока ждете мета-классы ...),

вот еще один дубль:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T> auto func(T&&... a)                            \
        -> typename NonConst<decltype(func(std::forward<T>(a)...))>::type   \
    {                                                                       \
        return const_cast<decltype(func(std::forward<T>(a)...))>(           \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

По сути, это смесь ответов @Pait, @DavidStone и @ sh1 (EDIT: и улучшение от @cdhowie). Что он добавляет в таблицу, так это то, что вы получаете только одну дополнительную строку кода, которая просто называет функцию (но без дублирования аргументов или возвращаемого типа):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

Примечание: gcc не может скомпилировать это до 8.1, clang-5 и выше, а также MSVC-19 счастливы (согласно обозреватель компилятора).

person axxel    schedule 29.03.2019
comment
Это просто сработало для меня. Это отличный ответ, спасибо! - person Short; 15.12.2019
comment
Разве decltype()s также не должны использовать std::forward в аргументах, чтобы убедиться, что мы используем правильный тип возвращаемого значения в случае, когда у нас есть перегрузки get(), которые принимают разные типы ссылок? - person cdhowie; 21.05.2020
comment
@cdhowie Можете привести пример? - person axxel; 21.05.2020
comment
@axxel Это чертовски надумано, но готово. Макрос NON_CONST неправильно определяет тип возвращаемого значения и const_cast относится к неправильному типу из-за отсутствия пересылки в типах decltype(func(a...)). Замена их на decltype(func(std::forward<T>(a)...)) решает эту проблему. (Это просто ошибка компоновщика, потому что я никогда не определял ни одну из объявленных перегрузок X::get.) - person cdhowie; 21.05.2020
comment
Спасибо @cdhowie, я изменил ваш пример, чтобы на самом деле использовать неконстантные перегрузки: coliru.stacked-crooked .com / a / 0cedc7f4e789479e - person axxel; 22.05.2020

Как насчет того, чтобы переместить логику в частный метод и делать только "получить ссылку и вернуть" внутри геттеров? На самом деле, я был бы довольно озадачен приведениями static и const внутри простой функции-получателя, и я бы счел это уродливым, за исключением чрезвычайно редких обстоятельств!

person MP24    schedule 23.09.2008
comment
Чтобы избежать неопределенного поведения, вам все равно понадобится const_cast. См. Ответ Мартина Йорка и мой комментарий там. - person Kevin; 24.09.2008
comment
Кевин, какой ответ Мартина Йорка - person Peter Nimmo; 03.03.2011

Использование препроцессора - это обман?

struct A {

    #define GETTER_CORE_CODE       \
    /* line 1 of getter code */    \
    /* line 2 of getter code */    \
    /* .....etc............. */    \
    /* line n of getter code */       

    // ^ NOTE: line continuation char '\' on all lines but the last

   B& get() {
        GETTER_CORE_CODE
   }

   const B& get() const {
        GETTER_CORE_CODE
   }

   #undef GETTER_CORE_CODE

};

Это не так изящно, как шаблоны или приведение типов, но оно делает ваше намерение («эти две функции должны быть идентичными») довольно явным.

person user1476176    schedule 14.01.2017
comment
Но тогда вы должны быть осторожны с обратной косой чертой (как обычно для многострочных макросов), и, кроме того, вы теряете подсветку синтаксиса в большинстве (если не во всех) редакторах. - person Ruslan; 23.01.2017

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

Я написал макрос FROM_CONST_OVERLOAD(), который можно поместить в неконстантную функцию для вызова константной функции.

Пример использования:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

Простая и многоразовая реализация:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

Объяснение:

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

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

Многих из этих шаблонов можно избежать, используя вывод типа. Во-первых, const_cast может быть инкапсулирован в WithoutConst(), который определяет тип его аргумента и удаляет квалификатор const. Во-вторых, аналогичный подход может быть использован в WithConst() для определения константы указателя this, что позволяет вызвать метод, перегруженный константой.

Остальное - это простой макрос, который ставит перед вызовом правильно определенный this-> и удаляет константу из результата. Поскольку выражение, используемое в макросе, почти всегда представляет собой простой вызов функции с перенаправленными аргументами 1: 1, недостатки макросов, такие как множественное вычисление, не проявляются. Можно также использовать многоточие и __VA_ARGS__, но они не нужны, потому что запятые ( как разделители аргументов) заключаются в круглые скобки.

У этого подхода есть несколько преимуществ:

  • Минимальный и естественный синтаксис - просто заключите вызов в FROM_CONST_OVERLOAD( )
  • Никаких дополнительных функций-членов не требуется
  • Совместим с C ++ 98
  • Простая реализация, без метапрограммирования шаблонов и нулевых зависимостей
  • Расширяемость: могут быть добавлены другие константные отношения (например, const_iterator, std::shared_ptr<const T> и т. Д.). Для этого просто перегрузите WithoutConst() для соответствующих типов.

Ограничения: это решение оптимизировано для сценариев, в которых неконстантная перегрузка делает то же самое, что и константная перегрузка, поэтому аргументы можно пересылать 1: 1. Если ваша логика отличается и вы не вызываете константную версию через this->Method(args), вы можете рассмотреть другие подходы.

person TheOperator    schedule 20.06.2019

Вот версия C ++ 17 статической вспомогательной функции шаблона с дополнительным тестом SFINAE.

#include <type_traits>

#define REQUIRES(...)         class = std::enable_if_t<(__VA_ARGS__)>
#define REQUIRES_CV_OF(A,B)   REQUIRES( std::is_same_v< std::remove_cv_t< A >, B > )

class Foobar {
private:
    int something;

    template<class FOOBAR, REQUIRES_CV_OF(FOOBAR, Foobar)>
    static auto& _getSomething(FOOBAR& self, int index) {
        // big, non-trivial chunk of code...
        return self.something;
    }

public:
    auto& getSomething(int index)       { return _getSomething(*this, index); }
    auto& getSomething(int index) const { return _getSomething(*this, index); }
};

Полная версия: https://godbolt.org/z/mMK4r3

person atablash    schedule 05.12.2019

Я бы предложил шаблон статической функции частного помощника, например:

class X
{
    std::vector<Z> vecZ;

    // ReturnType is explicitly 'Z&' or 'const Z&'
    // ThisType is deduced to be 'X' or 'const X'
    template <typename ReturnType, typename ThisType>
    static ReturnType Z_impl(ThisType& self, size_t index)
    {
        // massive amounts of code for validating index
        ReturnType ret = self.vecZ[index];
        // even more code for determining, blah, blah...
        return ret;
    }

public:
    Z& Z(size_t index)
    {
        return Z_impl<Z&>(*this, index);
    }
    const Z& Z(size_t index) const
    {
        return Z_impl<const Z&>(*this, index);
    }
};
person dats    schedule 07.10.2015

Хотя в большинстве ответов здесь предлагается использовать const_cast, в CppCoreGuidelines есть раздел об этом:

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

class Foo {
public:
    // not great, non-const calls const version but resorts to const_cast
    Bar& get_bar()
    {
        return const_cast<Bar&>(static_cast<const Foo&>(*this).get_bar());
    }
    const Bar& get_bar() const
    {
        /* the complex logic around getting a const reference to my_bar */
    }
private:
    Bar my_bar;
};

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

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

class Foo {
public:                         // good
          Bar& get_bar()       { return get_bar_impl(*this); }
    const Bar& get_bar() const { return get_bar_impl(*this); }
private:
    Bar my_bar;

    template<class T>           // good, deduces whether T is const or non-const
    static auto& get_bar_impl(T& t)
        { /* the complex logic around getting a possibly-const reference to my_bar */ }
};

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

person ivaigult    schedule 23.06.2021

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

person Dima    schedule 23.09.2008
comment
Это может быть правдой в большинстве случаев. Но бывают исключения. - person Kevin; 24.09.2008
comment
в любом случае, установщик констант не имеет особого смысла;) - person jwfearn; 24.09.2008
comment
Я имел в виду, что неконстантный геттер фактически является сеттером. :) - person Dima; 24.09.2008

Я сделал это для друга, который справедливо оправдал использование _1 _..., не зная об этом, я, вероятно, сделал бы что-то вроде этого (не очень элегантно):

#include <iostream>

class MyClass
{

public:

    int getI()
    {
        std::cout << "non-const getter" << std::endl;
        return privateGetI<MyClass, int>(*this);
    }

    const int getI() const
    {
        std::cout << "const getter" << std::endl;
        return privateGetI<const MyClass, const int>(*this);
    }

private:

    template <class C, typename T>
    static T privateGetI(C c)
    {
        //do my stuff
        return c._i;
    }

    int _i;
};

int main()
{
    const MyClass myConstClass = MyClass();
    myConstClass.getI();

    MyClass myNonConstClass;
    myNonConstClass.getI();

    return 0;
}
person matovitch    schedule 16.06.2015

Я придумал макрос, который автоматически генерирует пары константных / неконстантных функций.

class A
{
    int x;    
  public:
    MAYBE_CONST(
        CV int &GetX() CV {return x;}
        CV int &GetY() CV {return y;}
    )

    //   Equivalent to:
    // int &GetX() {return x;}
    // int &GetY() {return y;}
    // const int &GetX() const {return x;}
    // const int &GetY() const {return y;}
};

См. Конец ответа для реализации.

Аргумент MAYBE_CONST дублируется. В первом экземпляре CV ничего не заменяется; а во втором экземпляре он заменен на const.

Нет ограничений на то, сколько раз CV может появляться в аргументе макроса.

Хотя есть небольшое неудобство. Если CV появляется внутри круглых скобок, перед этой парой скобок должен стоять префикс CV_IN:

// Doesn't work
MAYBE_CONST( CV int &foo(CV int &); )

// Works, expands to
//         int &foo(      int &);
//   const int &foo(const int &);
MAYBE_CONST( CV int &foo CV_IN(CV int &); )

Реализация:

#define MAYBE_CONST(...) IMPL_CV_maybe_const( (IMPL_CV_null,__VA_ARGS__)() )
#define CV )(IMPL_CV_identity,
#define CV_IN(...) )(IMPL_CV_p_open,)(IMPL_CV_null,__VA_ARGS__)(IMPL_CV_p_close,)(IMPL_CV_null,

#define IMPL_CV_null(...)
#define IMPL_CV_identity(...) __VA_ARGS__
#define IMPL_CV_p_open(...) (
#define IMPL_CV_p_close(...) )

#define IMPL_CV_maybe_const(seq) IMPL_CV_a seq IMPL_CV_const_a seq

#define IMPL_CV_body(cv, m, ...) m(cv) __VA_ARGS__

#define IMPL_CV_a(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_b)
#define IMPL_CV_b(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_a)

#define IMPL_CV_const_a(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_b)
#define IMPL_CV_const_b(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_a)

Реализация до C ++ 20, которая не поддерживает CV_IN:

#define MAYBE_CONST(...) IMPL_MC( ((__VA_ARGS__)) )
#define CV ))((

#define IMPL_MC(seq) \
    IMPL_MC_end(IMPL_MC_a seq) \
    IMPL_MC_end(IMPL_MC_const_0 seq)

#define IMPL_MC_identity(...) __VA_ARGS__
#define IMPL_MC_end(...) IMPL_MC_end_(__VA_ARGS__)
#define IMPL_MC_end_(...) __VA_ARGS__##_end

#define IMPL_MC_a(elem) IMPL_MC_identity elem IMPL_MC_b
#define IMPL_MC_b(elem) IMPL_MC_identity elem IMPL_MC_a
#define IMPL_MC_a_end
#define IMPL_MC_b_end

#define IMPL_MC_const_0(elem)       IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a(elem) const IMPL_MC_identity elem IMPL_MC_const_b
#define IMPL_MC_const_b(elem) const IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a_end
#define IMPL_MC_const_b_end
person HolyBlackCat    schedule 19.10.2019

В этой статье о DDJ показан способ использования специализации шаблонов, который не требует использования const_cast. Однако для такой простой функции это действительно не нужно.

boost :: any_cast (в какой-то момент его больше нет) использует const_cast из константной версии, вызывая неконстантную версию, чтобы избежать дублирования. Вы не можете навязать константную семантику неконстантной версии, поэтому вы должны быть очень осторожны с этим.

В конце концов, некоторое дублирование кода допустимо, если два фрагмента находятся непосредственно друг над другом.

person Greg Rogers    schedule 23.09.2008
comment
Статья DDJ, похоже, относится к итераторам, что не имеет отношения к вопросу. Итераторы-константы не являются постоянными данными - это итераторы, указывающие на постоянные данные. - person Kevin; 24.09.2008

Чтобы добавить к решению, предоставленному jwfearn и kevin, вот соответствующее решение, когда функция возвращает shared_ptr:

struct C {
  shared_ptr<const char> get() const {
    return c;
  }
  shared_ptr<char> get() {
    return const_pointer_cast<char>(static_cast<const C &>(*this).get());
  }
  shared_ptr<char> c;
};
person Christer Swahn    schedule 20.11.2014

Не нашел то, что искал, поэтому накатил пару собственных ...

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

struct C {
  int x[10];

  int const* getp() const { return x; }
  int const* getp(int i) const { return &x[i]; }
  int const* getp(int* p) const { return &x[*p]; }

  int const& getr() const { return x[0]; }
  int const& getr(int i) const { return x[i]; }
  int const& getr(int* p) const { return x[*p]; }

  template<typename... Ts>
  auto* getp(Ts... args) {
    auto const* p = this;
    return const_cast<int*>(p->getp(args...));
  }

  template<typename... Ts>
  auto& getr(Ts... args) {
    auto const* p = this;
    return const_cast<int&>(p->getr(args...));
  }
};

Если у вас есть только один const метод для каждого имени, но все еще много методов, которые нужно дублировать, тогда вам может быть предпочтительнее следующее:

  template<typename T, typename... Ts>
  auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) {
    return const_cast<T*>((this->*f)(args...));
  }

  int* getp_i(int i) { return pwrap(&C::getp_i, i); }
  int* getp_p(int* p) { return pwrap(&C::getp_p, p); }

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

  template<typename... Ts>
  auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }

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

person sh1    schedule 15.12.2016