Вопрос:

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

c++ class const code-duplication c++-faq

27694 просмотра

17 ответа

13230 Репутация автора

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

class Z
{
    // details
};

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

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

        Z& ret = vecZ[index];

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

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

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

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

Автор: Kevin Источник Размещён: 23.09.2008 08:47

Ответы (17)


57 плюса

13230 Репутация автора

Решение

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

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

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

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

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

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

Автор: Kevin Размещён: 23.09.2008 08:48

-1 плюса

27786 Репутация автора

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

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

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

Автор: Greg Rogers Размещён: 23.09.2008 08:53

0 плюса

34096 Репутация автора

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

Автор: Dima Размещён: 23.09.2008 09:11

164 плюса

18602 Репутация автора

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

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

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

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

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

Автор: jwfearn Размещён: 23.09.2008 09:24

20 плюса

237227 Репутация автора

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

class X {

    private:

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

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

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

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

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

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

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

Автор: Steve Jessop Размещён: 23.09.2008 10:02

3 плюса

2318 Репутация автора

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

Автор: MP24 Размещён: 23.09.2008 10:05

6 плюса

3502 Репутация автора

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

.h файл:

#include <vector>

class Z
{
    // details
};

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

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

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

Файл .cpp:

#include "constnonconst.h"

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

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

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

    // ... even more code ...

    return ret;
}

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

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

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

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

Автор: Andy Balaam Размещён: 13.01.2009 04:21

31 плюса

621 Репутация автора

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

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

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

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

struct T {
   int arr[100];

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

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

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

Автор: Pait Размещён: 27.05.2013 08:43

16 плюса

8436 Репутация автора

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

class X {

private:

    std::vector<Z> v;

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

public:

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

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

};

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

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

Автор: gd1 Размещён: 08.12.2013 11:16

-1 плюса

37 Репутация автора

Чтобы добавить к решению 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;
};
Автор: Christer Swahn Размещён: 20.11.2014 02:06

0 плюса

791 Репутация автора

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

#include <iostream>

class MyClass
{

public:

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

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

private:

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

    int _i;
};

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

    MyClass myNonConstClass;
    myNonConstClass.getI();

    return 0;
}
Автор: matovitch Размещён: 16.06.2015 01:13

0 плюса

126 Репутация автора

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

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);
    }
};
Автор: dats Размещён: 07.10.2015 02:25

-1 плюса

3211 Репутация автора

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

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

struct C {
  int x[10];

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

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

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

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

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

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

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

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

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

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

Автор: sh1 Размещён: 15.12.2016 04:15

1 плюс

465 Репутация автора

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

struct A {

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

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

   B& get() {
        GETTER_CORE_CODE
   }

   const B& get() const {
        GETTER_CORE_CODE
   }

   #undef GETTER_CORE_CODE

};

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

Автор: user1476176 Размещён: 14.01.2017 02:33

18 плюса

13252 Репутация автора

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>
void as_mutable(T const &&) = delete;

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

T & f() {
    return as_mutable(std::as_const(*this).f());
}
Автор: David Stone Размещён: 18.11.2017 05:51

0 плюса

779 Репутация автора

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

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

Автор: axxel Размещён: 29.03.2019 10:19

0 плюса

2421 Репутация автора

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

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

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

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

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

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


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

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

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

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

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

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

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

Объяснение:

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

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

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

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

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

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

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

Автор: TheOperator Размещён: 20.06.2019 10:09
Вопросы из категории :
32x32