Як видалити дублювання коду між аналогічними функціями const і non-const?


242

Скажімо, у мене є таке, 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 {повернути z; }
Метт Ціна

1
Для основних типів ви абсолютно правильні! Мій перший приклад був не дуже хорошим. Скажімо, замість цього ми повертаємо якийсь екземпляр класу. (Я оновив питання, щоб це відобразити.)
Кевін,

Відповіді:


189

Детальне пояснення див. У розділі "Уникайте дублювання функцій constта не constчленів" на стор. 23, у пункті 3 "Використовуйте, constколи це можливо", в " Ефективному С ++" , 3-е видання Скотта Майєрса, ISBN-13: 9780321334879.

alt текст

Ось рішення Майєра (спрощено):

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

Два виклики і виклик функції можуть бути некрасивими, але це правильно. Майєрс має ґрунтовне пояснення чому.


45
Ніхто ніколи не звільнявся за наступного Скотта Майєрса :-)
Стів Джессоп

11
witkamp вірно, що взагалі погано використовувати const_cast. Це конкретний випадок, коли це не так, як пояснює Майєрс. @Adam: ROM => const добре. const == ROM - це явно нісенітниця, оскільки будь-хто може кидати non-const на constly willy-nilly: це рівнозначно просто вибору не змінювати щось.
Стів Джессоп

44
Як правило, я б запропонував використовувати const_cast замість static_cast, щоб додати const, оскільки це заважає вам випадково змінити тип.
Грег Роджерс

6
@HelloGoodbye: Я думаю , що Мейерс передбачає крапельку інтелекту від дизайнера інтерфейсу класу. Якщо get()constповертається щось, що було визначено як об'єкт const, тоді не повинно бути неконст-версії get(). Насправді моє роздуми щодо цього змінилося з часом: рішення шаблону - єдиний спосіб уникнути дублювання та отримати перевірену компілятором const-правильність, тому особисто я більше не використовував би те 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, щоб мати логіку і функція non-const викликати функцію члена const і повторно передавати значення повернення до посилання non-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
};

ПРИМІТКА. Важливо, щоб ви НЕ вводили логіку у функцію non-const і не викликали функцію const-функцію non-const - це може призвести до не визначеної поведінки. Причина полягає в тому, що постійний екземпляр класу отримує роль як непостійний екземпляр. Функція non-const може випадково змінити клас, у якому стандартні стани C ++ приведуть до невизначеної поведінки.


3
Нічого ... це жахливо. Ви просто збільшили кількість коду, зменшили чіткість і додали два sminkin 'const_cast <> s. Можливо, ви маєте на увазі приклад, де це насправді має сенс?
Shog9

14
Гей, не діг! Дивіться Ефективний C ++ , 3d видання, пункт 3 під заголовком "Уникання дублювання функцій const та non-cost member."
jwfearn

17
Хоча я розумію, що рішення може бути негарним, уявіть, що код, який визначає, що потрібно повернути, - це 50 рядків. Тоді дублювання вкрай небажано - особливо, коли вам доведеться перефабрикувати код. Я стикався з цим багато разів у своїй кар’єрі.
Кевін

8
Різниця між цим і Мейєрсом полягає в тому, що Мейєрс має static_cast <const X &> (* це). const_cast - це видалення const, а не його додавання.
Стів Джессоп

8
@VioletGiraffe ми знаємо, що спочатку об'єкт не був створений const, оскільки це non-const об'єкта non const, який ми знаємо, оскільки ми використовуємо метод non-const цього об'єкта. Компілятор не робить цього висновку, він дотримується консервативного правила. Як ви вважаєте, чому const_cast існує, якщо не для такої ситуації?
Калет

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

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

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


3
Соромно, що нам не треба std::remove_bottom_constйти std::remove_const.
TBBle

Мені не подобається це рішення, оскільки воно все ще вбудовує const_cast. Ви можете створити getElementсам шаблон і використовувати ознаку типу всередині mpl::conditionalпотрібних типів, наприклад, iterators або constiterators, якщо потрібно. Справжня проблема полягає в тому, як створити const версію методу, коли цю частину підпису неможливо шаблонувати?
v.oddou

2
@ v.oddou: std::remove_const<int const&>є int const &(зняти constкваліфікацію вищого рівня ), отже, NonConst<T>у цій відповіді гімнастика . Putative 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); }
};

У приватного методу є небажана властивість: він повертає не-const Z & для екземпляру const, тому він є приватним. Приватні методи можуть порушити інваріанти зовнішнього інтерфейсу (у цьому випадку бажаний інваріант "об'єкт const не може бути змінений за допомогою посилань, отриманих через нього на об'єкти, які він має-a").

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

До речі, я ціную рішення Майєрса. У мене немає філософського заперечення проти цього. Однак особисто я віддаю перевагу крихітному контрольованому повторенню та приватному методу, який потрібно викликати лише у певних жорстко контрольованих обставинах, ніж метод, схожий на шум лінії. Виберіть свою отруту і дотримуйтесь її.

[Редагувати: Кевін справедливо зазначив, що _getZ може захотіти викликати подальший метод (скажімо, GeneZ), який спеціально спеціалізується так само, як і getZ. У цьому випадку _getZ побачить const Z & і повинен буде const_cast перед тим, як повернутися. Це все-таки безпечно, оскільки прилад для котлоагрегатів контролює все, але не очевидно, що це безпечно. Крім того, якщо ви зробите це, а потім пізніше зміните generatorZ, щоб завжди повертати const, вам також потрібно змінити getZ, щоб завжди повертати const, але компілятор не скаже вам, що ви робите.

Остання точка щодо компілятора також стосується рекомендованої схеми Майєрса, але перший пункт про неочевидний const_cast не є. Таким чином, я вважаю, що якщо _getZ виявиться потрібним const_cast для його повернення, то ця модель втрачає багато свого значення над Мейєрсом. Оскільки він також зазнає недоліків порівняно з Мейєрсом, я думаю, я перейшов би до його в цій ситуації. Перейменовування з одного на інший є простим - це не впливає на будь-який інший дійсний код у класі, оскільки лише недійсний код та котла викликають _getZ.]


3
Це все ще має проблему, що те, що ви повертаєте, може бути постійним для постійного екземпляра X. У такому випадку вам все ще потрібен const_cast у _getZ (...). Якщо їх зловживають пізніші розробники, це все одно може призвести до розвитку UB. Якщо річ, яка повертається, "змінна", то це хороше рішення.
Кевін

1
Будь-яка приватна функція (чорт, і публічна) також може бути неправильно використана пізнішими розробниками, якщо вони захочуть ігнорувати інструкції BLOCK CAPITAL щодо її дійсного використання, у файлі заголовка, а також у Doxygen тощо. Я не можу це зупинити, і я не вважаю це моєю проблемою, оскільки інструкції легко зрозуміти.
Стів Джессоп

13
-1: Це не спрацьовує у багатьох ситуаціях. Що робити, якщо somethingу _getZ()функції є змінна інстанція? Компілятор (або принаймні деякі компілятори) поскаржиться, що оскільки _getZ()є const, будь-яка змінна інстанція, на яку посилається, є і const. Тож somethingбуде const (це буде тип const Z&) і не може бути перетворене на Z&. На моєму (правда, дещо обмеженому) досвіді, більшість часу somethingє змінною примірника у таких випадках.
Гравітація

2
@GravityBringer: тоді "щось" має включати const_cast. Він повинен був бути держателем місця для коду, необхідного для отримання повернення non-const від об'єкта 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змінної всередині нього.

Я не врахував усіх можливих (негативних) наслідків цього рішення. Будь ласка, дайте мені знати, якщо вони є.


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)Це ключове слово буде розпізнано як неявний аргумент екземпляра об'єкта, і дозволить компілятору визнати, що моя функція є членом або 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 () вище (з яких завжди потрібно мати const і non-const версія) або ви можете зробити цю функцію другом. Але я не люблю друзів.

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


3
Ви завжди можете зробити складну функцію реалізації статичним членом, щоб отримати доступ до приватних членів. Функцію потрібно задекларувати лише у файлі заголовка класу, визначення може міститись у файлі реалізації класу. Зрештою, це частина реалізації класу.
CB Bailey

Ага, гарна ідея! Мені не подобаються елементи шаблону, що з'являються у заголовку, але, оскільки тут це потенційно робить реалізацію набагато простішою, можливо, це варто.
Енді Валаам

+ 1 до цього рішення, яке не дублює жодного коду, а також не використовує жодного негарного const_cast(що випадково може бути використане для того, щоб перекинути те, що насправді повинно бути const на те, що немає).
HelloGoodbye

Сьогодні це можна спростити за допомогою виведеного типу повернення для шаблону (особливо корисно, оскільки воно зменшує те, що має бути дублюється в класі у випадку члена).
Девіс Оселедець

3

Як щодо перенесення логіки в приватний метод, і лише робити речі "отримати довідку та повернути" всередині геттера? Насправді, я був би досить розгублений щодо статичних і const ролей всередині простої функції геттера, і я вважав би це потворним за винятком надзвичайно рідкісних обставин!


Щоб уникнути невизначеної поведінки, вам все одно потрібен 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

};

Це не настільки фантазії, як шаблони чи касти, але це робить ваш намір ("ці дві функції мають бути однаковими") досить явним.


1
Але тоді вам слід бути обережними з косою рисою (як зазвичай для багаторядкових макросів), а крім того, ви втрачаєте виділення синтаксису у більшості (якщо не у всіх) редакторах.
Руслан

2

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

Я написав макрос, FROM_CONST_OVERLOAD()який можна розмістити у функції non-const, щоб викликати функцію const.

Приклад використання:

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)

Пояснення:

Як розміщено у багатьох відповідях, типовим шаблоном уникнення дублювання коду у функції non-const-члена є така:

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

Багато цього котла можна уникнути, використовуючи умовиводи типу. По-перше, він const_castможе бути інкапсульований WithoutConst(), що підводить тип його аргументу та видаляє const-класифікатор. По-друге, подібний підхід може бути використаний WithConst()для const-кваліфікації thisвказівника, що дозволяє викликати метод const-overloaded.

Решта - це простий макрос, який префіксує виклик з правильно кваліфікованим this->та видаляє const з результату. Оскільки вираз, який використовується в макросі, майже завжди є простим викликом функції з переадресованими аргументами 1: 1, недоліки макросів, таких як багаторазове оцінювання, не виникають. Еліпсис __VA_ARGS__також може бути використаний, але не повинен бути потрібним, оскільки коми (як роздільники аргументів) відбуваються в дужках.

Цей підхід має ряд переваг:

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

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


2

Для тих (як я), хто

  • використовувати 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 радіють (за даними провідника компілятора ).


Це просто спрацювало прямо для мене. Це чудова відповідь, дякую!
Короткий

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

@cdhowie Ви можете навести приклад?
axxel

@axxel Це задумано як пекло, але ось ви йдете . У NON_CONSTмакрос виводить тип повернення неправильно і const_castю до невідповідної з - за відсутності експедиції в decltype(func(a...))типах. Заміни їх decltype(func(std::forward<T>(a)...)) вирішує це . (Існує лише помилка лінкера, оскільки я ніколи не визначав жодну із заявлених X::getперевантажень.)
cdhowie

1
Дякую @cdhowie, я примножив ваш приклад, щоб фактично використовувати перенавантаження без обмежень
пікселя

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


1

Я придумав макрос, який автоматично генерує пари const / non-const функцій.

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

0

Зазвичай функціями-членами, для яких потрібні версії const та non-const, є getters та setters. Здебільшого вони є однолінійними, тому дублювання коду не є проблемою.


2
Це може бути правдою більшість часу. Але є винятки.
Кевін

1
так чи інакше, const setter не має особливого сенсу;)
jwfearn

Я мав на увазі, що неконст-геттер - це фактично сетер. :)
Діма

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

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, викликаючи версію non-const, щоб уникнути дублювання. Ви не можете нав'язувати семантику const на версії non-const, тому вам потрібно бути дуже обережним.

Зрештою який - то код дублювання в порядку до тих пір , як два сніпети розташовані один навпроти одного.


Стаття DDJ, схоже, стосується ітераторів - що не стосується питання. Конструктори-ітератори не є постійними даними - це ітератори, які вказують на постійні дані.
Кевін

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.