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

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

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

В этом примере я бы вернул значение в случае const, поэтому вы не сможете выполнить рефакторинг ниже. int Z () const {return z; }

Matt Price 24.09.2008 00:50

Что касается основных типов, вы абсолютно правы! Мой первый пример был не очень хорош. Допустим, вместо этого мы возвращаем какой-то экземпляр класса. (Я обновил вопрос, чтобы отразить это.)

Kevin 24.09.2008 01:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
258
2
39 901
19
Перейти к ответу Данный вопрос помечен как решенный

Ответы 19

Ответ принят как подходящий

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

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

Вау ... это ужасно. Вы просто увеличили объем кода, уменьшили ясность и добавили два stinkin 'const_cast <> s. Возможно, у вас есть пример, когда это действительно имеет смысл?

Shog9 24.09.2008 00:50

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

17 of 26 24.09.2008 00:55

Хорошо, вы изменили его, чтобы работать с чем-то другим, кроме простого целого числа. «Дублирующий» код Все еще более понятен, чем недублирующий код.

Shog9 24.09.2008 00:59

Эй, не говорите об этом !, это может быть некрасиво, но, по словам Скотта Мейерса, это (почти) правильный путь. См. Эффективный C++, 3-е изд., Пункт 3 под заголовком «Предотвращение дублирования в константных и не связанных с затратами функциях-членах».

jwfearn 24.09.2008 01:06

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

Kevin 24.09.2008 01:06

@jwfearn - Из любопытства, что не так? И чем мы со Скоттом Мейерсом различаемся?

Kevin 24.09.2008 01:07

Кевин - вам нужно как минимум редактирование №6 - в уродливом наборе приведений у вас есть "const_cast <int &>", который должен быть "const_cast <Z &>"

Michael Burr 24.09.2008 01:59

Разница между this и Meyers в том, что у Meyers есть static_cast <const X &> (* this). const_cast предназначен для удаления const, а не для его добавления.

Steve Jessop 24.09.2008 02:14

Согласно стандарту C++ нельзя использовать const_cast для удаления квалификатора const из объекта, который изначально был создан как const. Насколько я знаю, этот код вызывает неопределенное поведение.

Violet Giraffe 04.07.2016 14:11

@VioletGiraffe мы знаем, что объект изначально не был создан как const, поскольку он не является членом неконстантного объекта неконстантного объекта, что мы знаем, потому что мы находимся в неконстантном методе указанного объекта. Компилятор не делает этого вывода, он следует консервативному правилу. Как вы думаете, почему существует const_cast, если не для такой ситуации?

Caleth 15.03.2017 15:14

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

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

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

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

Kevin 24.09.2008 01:05

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

Это может быть правдой в большинстве случаев. Но бывают исключения.

Kevin 24.09.2008 01:14

в любом случае, установщик констант не имеет особого смысла;)

jwfearn 24.09.2008 01:46

Я имел в виду, что неконстантный геттер фактически является сеттером. :)

Dima 24.09.2008 02:00

Подробное объяснение см. В заголовке «Избегайте дублирования в функциях-членах const и не-const» на стр. 23, в пункте 3 «По возможности используйте const» в Эффективный C++, 3-е изд. Скотта Мейерса, 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. (Мейерс подробно обсуждает это.)

Никого не уволили за то, что он следил за Скоттом Мейерсом :-)

Steve Jessop 24.09.2008 02:15

const_cast почти никогда не следует использовать. Это нарушает постоянство. Это особенно актуально для встраиваемых систем, где у вас есть ПЗУ. В общем, использовать const_cast - плохая идея.

Ted 24.09.2008 04:03

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

Adam Mitz 24.09.2008 04:30

witkamp правильно, что вообще использовать const_cast плохо. Как объясняет Мейерс, это конкретный случай, когда это не так. @Adam: ROM => const в порядке. const == ROM - это, очевидно, ерунда, поскольку любой может волей-неволей преобразовать неконстантный в const: это эквивалентно простому выбору не изменять что-либо.

Steve Jessop 24.09.2008 06:35

В общем, я бы предложил использовать const_cast вместо static_cast для добавления const, поскольку это предотвращает случайное изменение типа.

Greg Rogers 23.11.2008 10:49

@witkamp Вы упускаете суть, есть два вида константности: физическая и логическая.

mloskot 15.11.2012 01:45

Что, если ваш const char & get() const возвращает что-то, что фактически является объявленным константой? Тогда ваша неконстантная версия get будет эффективно преобразовывать const_cast что-то, что никогда не должно было быть доступно для записи.

HelloGoodbye 28.03.2013 10:54

@HelloGoodbye: я думаю, что Мейерс предполагает наличие хоть интеллекта от дизайнера интерфейса класса. Если get()const возвращает что-то, что было определено как константный объект, тогда вообще не должно быть неконстантной версии get(). На самом деле, мое мышление по этому поводу изменилось со временем: шаблонное решение - единственный способ избежать дублирования. и получить проверенную компилятором корректность констант, поэтому лично я бы больше не использовал const_cast, чтобы избежать дублирования кода, я бы выбрал между помещая дублированный код в шаблон функции или оставляя его дублированным.

Steve Jessop 05.04.2013 13:37

Следующие два шаблона значительно улучшают читаемость этого решения: 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());

Casey Rodarmor 21.07.2014 16:46

Конечно, абсолютно эквивалентно, но я считаю этот вариант чуть более эстетичным: return const_cast<char &>(const_cast<const C *>(this)->get());

Aconcagua 26.06.2017 17:43

@CaseyRodarmor Теперь с C++ 17 std::as_const() лучше.

Deduplicator 19.03.2019 00:18

@GregRogers re: «В общем, я бы посоветовал использовать const_cast вместо static_cast для добавления const, поскольку это предотвращает случайное изменение типа». Это неправильно, поскольку const_cast предназначен для преобразования далеко в константу. Для преобразования к в константу следует использовать static_cast. Это дословно взято из книги Скотта Мейерса.

xdavidliu 20.10.2020 19:40

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

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.]

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

Kevin 24.09.2008 04:48

Когда я сказал, что '_getZ (...)' может использоваться неправильно, я имел в виду, что если будущий разработчик не понял, что он должен использоваться только общедоступными функциями, и вызвал его напрямую, но изменил значение в постоянный экземпляр X, то это может привести к UB. Это возможно, если не задокументировано.

Kevin 24.09.2008 04:50

Любая закрытая функция (черт возьми, общедоступная тоже) может быть неправильно использована более поздними разработчиками, если они решат игнорировать инструкции BLOCK CAPITAL при ее правильном использовании в файле заголовка, а также в Doxygen и т. д. Я не могу это остановить, и я не считаю это своей проблемой, так как инструкции просты для понимания.

Steve Jessop 24.09.2008 05:44

-1: Это не работает во многих ситуациях. Что, если something в функции _getZ() является переменной экземпляра? Компилятор (или, по крайней мере, некоторые компиляторы) будут жаловаться, что, поскольку _getZ() является константой, любая переменная экземпляра, на которую есть ссылка внутри, также является константой. Таким образом, something будет const (он будет иметь тип const Z&) и не сможет быть преобразован в Z&. По моему (правда, несколько ограниченному) опыту, в большинстве случаев something является переменной экземпляра в подобных случаях.

Gravity 05.08.2011 01:10

@GravityBringer: тогда "что-то" должно включать const_cast. Он был задуман как заполнитель для кода, необходимого для получения неконстантного возврата из объекта const, а не как заполнитель для того, что бы было в дублированном геттере. Итак, «что-то» - это не просто переменная экземпляра.

Steve Jessop 05.08.2011 12:54

Я понимаю. Однако это действительно снижает полезность техники. Я бы убрал отрицательный голос, но ТАК не позволит мне.

Gravity 07.08.2011 02:43

Этим достигается то же самое, что и у Мейерса, только ваш const_cast скрыт в something. Если бы something не должен был быть const_casted, это было бы потому, что его нельзя было использовать для изменения объекта, для которого был вызван метод, и тогда вам не понадобилась бы какая-либо неконстантная версия метода.

HelloGoodbye 04.04.2013 23:52

@HelloGoodbye: это не так. Неверно, что отдельные константные и неконстантные версии необходимы только в том случае, если требуется const_cast. Например, something может сводиться к *some_pointer_data_member (и предположим, что вы хотите вернуть ссылку на const в случае const), и в этом случае код Мейерса вводит приведение, но на самом деле он вам не нужен. Его код представляет собой совершенно общий шаблон, но большинство актуальных функций, которые вы пишете, более конкретны :-)

Steve Jessop 05.04.2013 13:26

Что ж, я думаю, в этом случае вы можете изменить свой собственный экземпляр класса даже из метода const, который вызывается в экземпляре, по крайней мере, если член-указатель указывает на член в экземпляре или на сам экземпляр, поскольку компилятор не знать, к чему обращается указатель. Меня это не поразило до сих пор.

HelloGoodbye 05.04.2013 17:24

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

alfC 19.10.2015 21:45

@HelloGoodbye, где скрытый const_cast? Если возвращенная вещь не является константой, код совершенно правильный. Суть этого ответа в том, что он не использует явный или неявный const_cast. Он достигает эффекта, просто нарушая обычное соглашение, но делает это в своей частной реализации.

alfC 19.10.2015 21:50

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

Чтобы избежать неопределенного поведения, вам все равно понадобится const_cast. См. Ответ Мартина Йорка и мой комментарий там.

Kevin 24.09.2008 02:28

Кевин, какой ответ Мартина Йорка

Peter Nimmo 03.03.2011 21:46

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

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

CB Bailey 13.01.2009 20:49

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

Andy Balaam 14.01.2009 12:15

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

HelloGoodbye 05.04.2013 00:01

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

Davis Herring 19.03.2020 01:15

Я думаю, что решение Скотта Мейерса можно улучшить в 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 из-за вариативных шаблонов.

Жаль, что у нас нет std::remove_bottom_const для std::remove_const.

TBBle 04.02.2016 06:58

Мне не нравится это решение, потому что в нем все еще встроен const_cast. Вы можете сделать getElement шаблоном и использовать признак типа внутри для типов mpl::conditional, которые вам нужны, например, iterators или constiterators, если необходимо. Настоящая проблема в том, как сгенерировать константную версию метода, если эта часть подписи не может быть шаблонизирована?

v.oddou 04.03.2016 06:54

@ 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 04.03.2016 11:22

@ v.oddou: Я думаю, вы не можете обойтись без двух отдельных объявлений по той причине, которую вы упомянули.

Pait 04.03.2016 12:51

И никто не должен использовать это решение в производственном ПО. Это слишком "хитро". Либо продублируйте свой код, если он достаточно простой, либо используйте решение Скотта Мейера, потому что это уродливая, но хорошо известная идиома.

Pait 04.03.2016 12:59

Это решение не работает, если getElement перегружен. Тогда указатель на функцию не может быть разрешен без явного указания параметров шаблона. Почему?

John 20.06.2016 17:09

Я согласен с Джоном. Для поддержки вот тест: ideone.com/fwqmsB. В настоящее время он компилируется, но если раскомментировать / ** / (функция перегрузки), он больше не будет компилироваться. Было бы неплохо, если бы это решение было немного улучшено.

javaLover 22.03.2017 08:51

@John Это проблема с разрешением перегрузки C++ для указателей на функции-члены (см. Также stackoverflow.com/questions/2942426/…). В настоящее время это может быть решено только на стороне вызывающего абонента. Однако указывать все параметры шаблона необязательно. Только первый TConstReturn тоже работает. Нравится: return likeConstVersion<int const&>(this, &T::getElement, i);

Pait 11.04.2017 13:42

Вам необходимо исправить свой ответ, чтобы использовать C++ 11 perfect forwarding: likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TA‌​rgs>(args)...)); } Complete: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83

ShaulF 18.05.2017 20:34

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

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.

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

Что ж, давайте остановимся на том простом факте, что вы добавили больше шаблонов. Во всяком случае, это следует использовать как пример того, почему языку нужен способ изменения квалификаторов функций вместе с возвращаемым типом auto get(std::size_t i) -> auto(const), auto(&&). Почему '&&'? Ах, так что могу сказать: auto foo() -> auto(const), auto(&&) = delete;

kfsone 28.04.2015 02:40

gd1: именно то, что я имел в виду. @kfsone и то, что я тоже сделал.

v.oddou 04.03.2016 06:57

@kfsone синтаксис должен включать ключевое слово this. Я предлагаю template< typename T > auto myfunction(T this, t args) -> decltype(ident). Ключевое слово this будет распознано как неявный аргумент экземпляра объекта и позволит компилятору распознать, что myfunction является членом или T. T будет автоматически выведен на сайте вызова, который всегда будет типом класса, но с бесплатной квалификацией cv.

v.oddou 04.03.2016 06:58

Это решение соответствует stackoverflow.com/a/38751554/259543. То есть статическая функция-член для интерполяции квалификаторов *this с использованием универсальных ссылок.

alecov 11.08.2016 01:24

Это решение также имеет то преимущество (по сравнению с const_cast), что позволяет возвращать iterator и const_iterator.

Jarod42 08.08.2017 14:47

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

Jarod42 08.08.2017 14:51

@ Jarod42 ммм частные участники?

gd1 08.08.2017 14:53

@ gd1: Вы все еще можете передать их в дополнение / замену *this, если это необходимо.

Jarod42 08.08.2017 14:58

@ Jarod42 немного некрасивый

gd1 08.08.2017 15:00

return get_impl(v, i); против return get_impl(*this, i); - это не IMO, но return get_impl(*this, v, member1, member2, i); будет, и в этом случае статический член шаблона все еще доступен (и может быть реализован в файле cpp (с соответствующими геттерами), который используется только в 2 местах).

Jarod42 08.08.2017 15:07

Это лучший способ, который я знаю, определенно должен быть принятый ответ. Сам набирал, а вот эту уже нашел. Вот пример C++ 17 с добавленным необязательным тестом SFINAE: godbolt.org/z/mMK4r3

atablash 05.12.2019 21:17

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

iPherian 16.01.2020 15:24

Это решение, которое следует использовать (+/- регулировки), если у вас есть побочные эффекты, кстати. Как, например, при реализации нединамического шаблона посетителя, когда посетитель может делать разные вещи в зависимости от константности объекта (читать, когда константа, писать, когда не константа).

Victor Drouin 07.12.2020 19:29

Чтобы добавить к решению, предоставленному 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;
};

Я сделал это для друга, который справедливо оправдал использование 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;
}

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

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);
    }
};

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

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

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

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

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

};

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

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

Ruslan 23.01.2017 17:01

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&.

Max Truxa 25.07.2018 13:59

@MaxTruxa: Да, и это хорошо. Если бы он просто скомпилировался, у нас была бы свисающая ссылка. В случае, когда f() возвращает T, мы не хотим иметь две перегрузки, достаточно только версии const.

David Stone 26.07.2018 17:13

Совершенно верно, я прошу прощения за свой вчерашний мозговой пук, понятия не имею, о чем я думал, когда писал этот комментарий. Я смотрел на пару констант / изменяемых геттеров, возвращающих shared_ptr. Так что мне действительно нужно было что-то вроде as_mutable_ptr, который выглядит почти идентично as_mutable выше, за исключением того, что он принимает и возвращает shared_ptr и использует std::const_pointer_cast вместо const_cast.

Max Truxa 26.07.2018 17:42

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

monkey0506 12.08.2019 06:15

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

monkey0506 08.09.2019 20:53

@ monkey0506: Я обновил свой ответ, добавив в него указатели и ссылки.

David Stone 10.09.2019 05:11

Вы упомянули, что ваше первое решение (использующее std::as_const) может случайно избавиться от volatile. Вышеупомянутый ответ (Скотт Мейер) также страдает от этой проблемы?

user2023370 17.04.2020 16:41

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

David Stone 18.04.2020 09:43

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

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

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

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

Это просто сработало для меня. Это отличный ответ, спасибо!

Short 15.12.2019 19:27

Разве decltype()s не должны также использовать std::forward для аргументов, чтобы убедиться, что мы используем правильный тип возвращаемого значения в случае, когда у нас есть перегрузки get(), которые принимают различные типы ссылок?

cdhowie 21.05.2020 10:38

@cdhowie Можете привести пример?

axxel 21.05.2020 17:47

@axxel Чертовски надуманный, но Ну вот. Макрос NON_CONST неправильно определяет тип возвращаемого значения, а const_cast - неправильный тип из-за отсутствия пересылки в типах decltype(func(a...)). Замена их на decltype(func(std::forward<T>(a)...))решает это. (Это просто ошибка компоновщика, потому что я никогда не определял ни одну из заявленных перегрузок X::get.)

cdhowie 21.05.2020 19:50

Спасибо @cdhowie, я изменил ваш пример, чтобы на самом деле использовать неконстантные перегрузки: coliru.stacked-crooked.com/a/0cedc7f4e789479e

axxel 22.05.2020 12:48

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

Я написал макрос 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. Если ваша логика отличается и вы не вызываете константную версию через this->Method(args), вы можете рассмотреть другие подходы.

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

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

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

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