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

243

Допустим, у меня есть следующее, 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имеют одинаковый код внутри фигурных скобок. Это дублирующий код и может вызвать проблемы с обслуживанием для длинных функций со сложной логикой .

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

Kevin
источник
В этом примере я бы вернул значение в константном случае, поэтому вы не сможете выполнить рефакторинг ниже. int Z () const {return z; }
Мэтт Прайс
1
Для фундаментальных типов вы абсолютно правы! Мой первый пример был не очень хорошим. Допустим, вместо этого мы возвращаем некоторый экземпляр класса. (Я обновил вопрос, чтобы отразить это.)
Кевин

Ответы:

190

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

альтернативный текст

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

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

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

jwfearn
источник
45
Никто никогда не был уволен за то, что следил за Скоттом Мейерсом :-)
Стив Джессоп
11
witkamp правильно, что вообще плохо использовать const_cast. Это особый случай, когда это не так, как объясняет Мейерс. @Adam: ROM => const в порядке. const == ROM, очевидно, бессмыслица, поскольку любой может привести неконстантность к const волей-неволей: это равносильно тому, чтобы просто не изменять что-либо.
Стив Джессоп
44
В общем, я бы предложил использовать const_cast вместо static_cast для добавления const, так как это предотвращает случайное изменение типа.
Грег Роджерс
6
@HelloGoodbye: Я думаю, что Мейерс предполагает немного интеллекта от дизайнера интерфейса класса. Если get()constвозвращает что-то, что было определено как const-объект, тогда не должно быть неконстантной версии get()вообще. На самом деле мое мышление об этом со временем изменилось: шаблонное решение - единственный способ избежать дублирования и получить проверенную компилятором константность, поэтому лично я бы больше не использовал const_cast, чтобы избежать дублирования кода, я бы выбрал установку дублированный код в шаблон функции или оставив его дублированным.
Стив Джессоп
7
Следующие два шаблона очень помогают в удобочитаемости этого решения: 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());
Кейси Родармор
64

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

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 ++ приведет к неопределенному поведению.

Кевин
источник
3
Вау ... это ужасно Вы просто увеличили объем кода, понизили четкость и добавили два вонючих const_cast <>. Возможно, вы имеете в виду пример, где это на самом деле имеет смысл?
Shog9
14
Эй, не звоните! Это может быть некрасиво, но, по словам Скотта Мейерса, это (почти) правильный путь. См. Эффективный C ++ , 3-е издание, пункт 3 под заголовком «Как избежать дублирования в функциях const и non-cost-элементах»
jwfearn,
17
Хотя я понимаю, что решение может быть уродливым, представьте, что код, определяющий, что нужно вернуть, имеет длину 50 строк. В этом случае дублирование крайне нежелательно, особенно если вам необходимо повторно кодировать код. Я сталкивался с этим много раз в моей карьере.
Кевин
8
Разница между этим и Meyers заключается в том, что у Meyers есть static_cast <const X &> (* this). const_cast предназначен для удаления const, а не для его добавления.
Стив Джессоп
8
@VioletGiraffe мы знаем, что объект изначально не был создан const, так как он является неконстантным членом неконстантного объекта, который мы знаем, потому что мы используем неконстантный метод указанного объекта. Компилятор не делает этот вывод, он следует консервативному правилу. Как вы думаете, почему const_cast существует, если не для такой ситуации?
Caleth
47

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());
}
Дэвид Стоун
источник
Обратите внимание, что «as_mutable» с удаленной перегрузкой const rvalue (что, как правило, предпочтительнее) не позволяет последнему примеру работать, если f()возвращается Tвместо T&.
Макс Трукса
1
@MaxTruxa: Да, и это хорошо. Если бы это только скомпилировано, у нас была бы свисающая ссылка. В случае f()возврата T, мы не хотим иметь две перегрузки, constодной версии достаточно.
Дэвид Стоун
Совершенно верно, я прошу прощения за мой полный пердеть мозг вчера, понятия не имею, о чем я думал, когда я написал этот комментарий. Я смотрел на получающую пару const / mutable, возвращающую a shared_ptr. Так что на самом деле мне нужно было что-то вроде того, as_mutable_ptrчто выглядит почти так же, как as_mutableуказано выше, за исключением того, что оно принимает и возвращает shared_ptrи использует std::const_pointer_castвместо const_cast.
Макс Трукса
1
Если метод возвращается, T const*то это будет T const* const&&скорее привязка, чем привязка T const* const&(по крайней мере, в моем тестировании это было сделано). Мне пришлось добавить перегрузку для T const*в качестве типа аргумента для методов, возвращающих указатель.
monkey0506
2
@ monkey0506: я обновил свой ответ для поддержки указателей, а также ссылок
Дэвид Стоун
34

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

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 из-за шаблонов с переменным числом аргументов.

Pait
источник
3
Это позор, с которым нам не нужно std::remove_bottom_constидти std::remove_const.
TBBle
Мне не нравится это решение, потому что оно все еще встраивает const_cast. Вы можете создать getElementсам шаблон и использовать черту типа внутри для mpl::conditionalтипов, которые вам нужны, например, iterators или constiterators, если это необходимо. Реальная проблема заключается в том, как сгенерировать постоянную версию метода, когда эта часть сигнатуры не может быть шаблонизирована?
v.oddou
2
@ v.oddou: std::remove_const<int const&>есть int const &(удалить constквалификацию высшего уровня ), отсюда и гимнастика NonConst<T>в этом ответе. Предполагаемый std::remove_bottom_constможет удалить constквалификацию нижнего уровня , и делать именно то, NonConst<T>что здесь: std::remove_bottom_const<int const&>::type=> int&.
TBBle
4
Это решение не работает, если getElementперегружено. Тогда указатель на функцию не может быть разрешен без явного указания параметров шаблона. Зачем?
Джон
1
Вы должны исправить свой ответ, чтобы использовать идеальную пересылку 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)...)); }Завершено: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83
ShaulF
22

Немного более многословно, чем Мейерс, но я мог бы сделать это:

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 & для константного экземпляра, поэтому он является закрытым. Частные методы могут нарушать инварианты внешнего интерфейса (в этом случае желаемый инвариант - «объект const не может быть изменен через ссылки, полученные через него на объекты, которые он имеет -a»).

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

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

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

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

Стив Джессоп
источник
3
По-прежнему существует проблема, заключающаяся в том, что возвращаемая вещь может быть постоянной для постоянного экземпляра X. В этом случае вам все еще требуется const_cast в _getZ (...). Если его неправильно используют более поздние разработчики, это все равно может привести к UB. Если возвращаемая вещь является «изменяемой», то это хорошее решение.
Кевин
1
Более поздние разработчики могут неправильно использовать любую частную функцию (черт возьми, тоже открытую), если они решат игнорировать инструкции BLOCK CAPITAL относительно ее действительного использования, в заголовочном файле, а также в Doxygen и т. Д. Я не могу это остановить, и я не считаю это своей проблемой, так как инструкции легко понять.
Стив Джессоп
13
-1: это не работает во многих ситуациях. Что если somethingв _getZ()функции есть переменная экземпляра? Компилятор (или, по крайней мере, некоторые компиляторы) будут жаловаться на то, что, поскольку он _getZ()является константой, любая переменная экземпляра, на которую ссылается внутри, тоже является константой. Так somethingчто тогда будет const (это будет тип const Z&) и не может быть преобразован в Z&. По моему (по общему признанию, несколько ограниченному) опыту, большую часть времени somethingявляется переменной экземпляра в подобных случаях.
гравитация
2
@GravityBringer: тогда «что-то» должно включать в себя const_cast. Предполагалось, что он будет заполнителем для кода, необходимого для получения неконстантного возврата от объекта const, а не как заполнитель для того, что было бы в дублированном геттере. Так что «что-то» - это не просто переменная экземпляра.
Стив Джессоп
2
Понимаю. Это действительно уменьшает полезность техники, хотя. Я бы убрал понижающий голос, но ТАК не позволит мне.
Гравитация
22

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

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переменной внутри него.

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

gd1
источник
4
Что ж, давайте перейдем к тому простому факту, что вы добавили больше шаблона. Во всяком случае, это следует использовать в качестве примера того, почему языку нужен способ изменения классификаторов функций вместе с типом возвращаемого значения auto get(std::size_t i) -> auto(const), auto(&&). Зачем '&&'? Ааа, так что я могу сказать:auto foo() -> auto(const), auto(&&) = delete;
kfsone
gd1: именно то, что я имел в виду. @kfsone и именно то, что я тоже сделал вывод.
v.oddou
1
@kfsone синтаксис должен включать thisключевое слово. Я предлагаю template< typename T > auto myfunction(T this, t args) -> decltype(ident)ключевое слово this будет распознано как неявный аргумент экземпляра объекта и позволит компилятору распознать, что myfunction является членом или T. Tбудет автоматически выведен на сайт вызова, который всегда будет типом класса, но с бесплатной квалификацией cv.
v.oddou
2
Это решение также имеет преимущество (по сравнению с const_castодним), позволяющее вернуться iteratorи const_iterator.
Jarod42
1
Если реализация перемещается в файл cpp (и поскольку метод, который не должен дублироваться, не должен быть тривиальным, вероятно, так и будет), это staticможно сделать в области видимости файла вместо области видимости класса. :-)
Jarod42
8

Вы также можете решить это с помощью шаблонов. Это решение немного уродливо (но уродство скрыто в файле .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 () выше (из которых всегда должен быть константная и неконстантная версия) или вы можете сделать эту функцию другом. Но я не люблю друзей.

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

Энди Валаам
источник
3
Вы всегда можете сделать сложную функцию реализации статическим членом, чтобы получить доступ к закрытым членам. Функция должна быть объявлена ​​только в файле заголовка класса, определение может находиться в файле реализации класса. В конце концов, это часть реализации класса.
CB Bailey
Ааа да хорошая идея! Мне не нравится, что шаблонные материалы появляются в заголовке, но если с тех пор здесь это потенциально значительно упрощает реализацию, это, вероятно, того стоит.
Энди Валаам
+1 к этому решению, которое не дублирует какой-либо код и не использует уродливое const_cast(которое может случайно использоваться, чтобы заменить что-то, что на самом деле должно совпадать с чем-то, что не является).
Здравствуйте, до свидания,
В настоящее время это можно упростить с помощью выведенного типа возвращаемого значения для шаблона (особенно полезно, поскольку оно уменьшает то, что должно дублироваться в классе в случае члена).
Дэвис Херринг
3

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

МР24
источник
Чтобы избежать неопределенного поведения, вам все еще нужен const_cast. Смотрите ответ Мартина Йорка и мой комментарий там.
Кевин
1
Кевин, какой ответ Мартин Йорк
Питер Ниммо
2

Это обманывает использовать препроцессор?

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

};

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

user1476176
источник
1
Но тогда вы должны быть осторожны с обратной косой чертой (как обычно для многострочных макросов) и, кроме того, вы теряете подсветку синтаксиса в большинстве (если не во всех) редакторах.
Руслан
2

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

Я написал макрос, 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->и удаляет const из результата. Поскольку выражение, используемое в макросе, почти всегда является простым вызовом функции с переадресованными аргументами 1: 1, недостатки макросов, такие как множественная оценка, не устраняются. Многоточие __VA_ARGS__также может использоваться, но не должно быть необходимо, потому что запятые (как разделители аргументов) встречаются в скобках.

Этот подход имеет несколько преимуществ:

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

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

Оператор
источник
2

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

  • использовать с ++ 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 ( РЕДАКТИРОВАТЬ : и улучшение от @cdhowie). То, что он добавляет в таблицу, - это то, что вы получаете только одну дополнительную строку кода, которая просто называет функцию (но без аргумента или дублирования возвращаемого типа):

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

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

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

Вот версия 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

atablash
источник
1

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

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
HolyBlackCat
источник
0

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

Дима
источник
2
Это может быть правдой большую часть времени. Но есть и исключения.
Кевин
1
в любом случае,
метод
Я имел в виду, что неконстантный геттер по сути является сеттером. :)
Дима
0

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

#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;
}
matovitch
источник
0

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

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);
    }
};
Даць
источник
-1

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

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

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

Грег Роджерс
источник
Кажется, что статья DDJ ссылается на итераторы, что не имеет отношения к вопросу. Const-итераторы не являются постоянными данными - они являются итераторами, которые указывают на постоянные данные.
Кевин
-1

Чтобы добавить к решению 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;
};
Кристер Суон
источник
-1

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

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

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методу не совпадают с внешне-значимыми аргументами шаблона, и он ломается. Не уверен почему. Вот почему .

sh1
источник