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

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

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

Ответ 1

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

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

Ответ 2

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

alt text

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

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

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

Ответ 3

Я думаю, что решение Скотта Мейерса можно улучшить в С++ 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);
   }
};

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

Ответ 4

С++ 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());
}

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

Это мошенничество для использования препроцессора?

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

};

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

Ответ 10

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

  • использовать 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(a...))>::type {  \
        return const_cast<decltype(func(a...))>(                            \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

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

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

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

Ответ 11

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

Я написал макрос 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() для const-квалификации указателя this, который позволяет вызывать перегруженный const метод.

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

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

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

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

Ответ 12

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

Ответ 13

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

Ответ 14

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

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

Ответ 15

Вот что я придумала:

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

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

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

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


Реализация:

#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

Ответ 16

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

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

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

Ответ 17

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

Ответ 18

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

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

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