Bezdepbonusu7Программирование: статьи и ответы

Вопрос

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

Автор вопроса: KevinОтветов: 12

Допустим, у меня есть следующий 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
  • Что касается основных типов, вы абсолютно правы! Мой первый пример был не очень хорош. Допустим, вместо этого мы возвращаем какой-то экземпляр класса. (Я обновил вопрос, чтобы отразить это.) Kevin 24.09.2008

c++classcode-duplicationconstantsc++-faq

Ответы

Ответ 1 · рейтинг 206 · jwfearn

Подробное объяснение см. В разделе "Избегайте дублирования в const и не const функциях-членах" на стр. 23, в п. 3 По возможности используйте const в Effective C ++, 3d ed a> Скотта Мейерса, 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;
};

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

Ответ 2 · рейтинг 69 · Community

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

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

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

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

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

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

Ответ 3 · рейтинг 63 · David Stone

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

Ответ 4 · рейтинг 36 · Pait

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

Ответ 5 · рейтинг 27 · gd1

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

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.

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

Ответ 6 · рейтинг 22 · Steve Jessop

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

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

Ответ 7 · рейтинг 8 · Andy Balaam

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

Ответ 8 · рейтинг 5 · axxel

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

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

Ответ 9 · рейтинг 3 · MP24

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

Ответ 10 · рейтинг 2 · user1476176

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

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

};

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

Ответ 11 · рейтинг 2 · TheOperator

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

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

У этого подхода есть несколько преимуществ:

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

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

Ответ 12 · рейтинг 2 · atablash

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

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы