Страницы

Поиск по вопросам

понедельник, 2 декабря 2019 г.

Исключить дублирование кода в функциях с разной константностью

#cpp #рефакторинг


Рассмотрим такой код:

struct B {};
struct D1 : B {};
struct D2 : B {};

#define get if (s) return d1; else return d2;

volatile bool s;

struct C {
    const B& f() const { get }
    B& f() { get } 
private:
    D1 d1;
    D2 d2;
};


Здесь видно, что разные версии f() должны возвращать один и тот же объект (в одном
случае - константный, в другом - нет), основываясь на некоторой логике выбора, которая
может быть достаточно сложной. В примере для исключения дублирования кода этой логики
использована макроподстановка через #define. 

Можно ли избежать дублирования кода в разных версиях f() не прибегая к услугам препроцессора?
    


Ответы

Ответ 1



Проблема дублирования кода из-за соображений константности обычно встречается в двух вариантах: На уровне отдельных функций: когда есть две функции с одинаковой реализацией, отличающиеся лишь константностью входных типов (в т.ч., как частный случай, константностью *this в методе класса) и соответствующей константностью возвращаемого значения. В языке С одной из известных идиом для решения этой проблемы является написание одной-единственной функции, которая решает поставленную задачу в рамках соблюдения константности входных данных, а затем просто безусловно снимает константность с возвращаемого значения (см., например, стандартную функцию strstr). В данном случае предполагается, что вызывающий код, будучи в курсе ситуации с константностью данных, "по-джентельменски" вернет "потерянную" в процессе вызова функции константность на место. В языке С++ этот подход технически тоже применим, но его использовать не принято. Точнее, традиционная С++ идиома, основанная внутренне фактически на том же самом подходе, внешне реализуется с небольшим отличием: полноценная реализация предоставляется для константной версии функции, а над ней надстраивается вторая - неконстантная - версия той же функции. Последняя реализуется через константную путем снятия константности с возвращаемого значения const return_type *foo(const input_type *argument) { ... } return_type *foo(input_type *argument) { return const_cast(foo(const_cast(argument)); } Вот именно этот подход прекрасно подойдет в вашем случае. На уровне отдельных классов: кода надо реализовать два класса, которые фактически идентичны с точки зрения исходного кода, а отличаются лишь внешней константностью обрабатываемых данных. Хороший пример: константная и неконстантная версия класса контейнерного итератора. В такой ситуации один из жизнеспособных подходов - реализация общей функциональности в виде шаблонного класса, параметризованного необходимым количеством типов (в простейшем случае - одним), и реализация требуемых финальных классов через специализации этого шаблона. Что-то вроде template class list { template class iterator_impl { ... }; typedef iterator_impl iterator; typedef iterator_impl const_iterator; ... }; P.S. Ваш собственный ответ, использующий шаблонную функцию - это фактически адаптирование вышеприведенного второго подхода к первой ситуации. Работать, без сомнения, будет, однако именно в такой ситуации банальный вариант с const_cast, как мне кажется, выглядит проще и уместнее.

Ответ 2



Можно написать что-то вроде следующего struct C { const B& f() const { if (s) return d1; else return d2; } B& f() { return const_cast( const_cast( this )->f() ); } private: D1 d1; D2 d2; }; Вот демонстрационная программа #include struct B {}; struct D1 : B {}; struct D2 : B {}; bool s; struct C { const B& f() const { std::cout << "const B & f() const" << std::endl; if (s) return d1; else return d2; } B& f() { std::cout << "B & f()" << std::endl; return const_cast( const_cast( this )->f() ); } private: D1 d1; D2 d2; }; int main() { C c1; c1.f(); std::cout << std::endl; const C c2; c2.f(); return 0; } Ее вывод на консоль B & f() const B & f() const const B & f() const

Ответ 3



Придумал вариант с шаблонной дружественной функцией: template R& g(T* t); struct C { const B& f() const { return g(this); } B& f() { return g(this); } private: D1 d1; D2 d2; template friend R& g(T* t); }; template R& g(T* t) { if (s) return t->d1; else return t->d2; } Или можно вовсе перенести в класс: struct C { const B& f() const; B& f(); private: D1 d1; D2 d2; template static R& g(T* t) { if (s) return t->d1; else return t->d2; } }; const B& C::f() const { return g(this); } B& C::f() { return g(this); } А т.к тип R по сути может быть выведен из факта наличия константности в T, то этот тип можно вовсе убрать из шаблона: template static std::conditional_t, const B&, B&> g(T* t) { if (s) return t->d1; else return t->d2; } Т.о. необходимость явно указывать тип при вызове g отпадает: const B& C::f() const { return g(this); } B& C::f() { return g(this); }

Ответ 4



Очевидный вариант: class A { bool flag; int a; int b; public: int& get() { return flag ? a : b; } const int& get() const { return ((A*)this)->get(); } }; Пример: int main() { A a {}; const A ca {}; static_assert(std::is_same::value, "!!"); static_assert(std::is_same::value, "!!"); }

Комментариев нет:

Отправить комментарий