«Вперед неразрушимые» шаблоны классов доступа [C ++] - PullRequest
2 голосов
/ 07 января 2010

Если я не ошибаюсь, шаблон getter / setter - это общий шаблон, используемый для двух вещей:

  1. Чтобы сделать личную переменную так, чтобы ее можно было использовать, но никогда не изменять, путем предоставления только метода getVariable (или, реже, только модифицируемого, только путем предоставления метода setVariable).
  2. Чтобы быть уверенным, что в будущем, если у вас возникнет проблема, для которой хорошим решением было бы просто обработать переменную до того, как она войдет и / или выйдет из класса, вы можете обработать переменную с помощью фактическая реализация методов получения и установки вместо простого возврата или установки значений. Таким образом, изменение не распространяется на остальную часть кода.

Вопрос № 1: Я пропускаю какие-либо средства доступа или мои предположения неверны? Я не уверен, что я прав в этом.

Вопрос № 2: Существуют ли какие-либо шаблоныные качества, которые могут помешать мне написать средства доступа для моих переменных-членов? Я не нашел ни одного.

Вопрос № 3: Будет ли следующий шаблон класса хорошим способом реализации геттера без необходимости фактически писать аксессор?

template <class T>
struct TemplateParameterIndirection // This hack works for MinGW's GCC 4.4.1, dunno others
{
    typedef T Type;
};

template <typename T,class Owner>
class Getter
{
public:
    friend class TemplateParameterIndirection<Owner>::Type; // Befriends template parameter

    template <typename ... Args>
    Getter(Args args) : value(args ...) {} // Uses C++0x

    T get() { return value; }

protected:
    T value;
};

class Window
{
public:
    Getter<uint32_t,Window> width;
    Getter<uint32_t,Window> height;

    void resize(uint32_t width,uint32_t height)
    {
        // do actual window resizing logic

        width.value = width; // access permitted: Getter befriends Window
        height.value = height; // same here
    }
};

void someExternalFunction()
{
    Window win;

    win.resize(640,480); // Ok: public method

    // This works: Getter::get() is public
    std::cout << "Current window size: " << win.width.get() << 'x' << win.height.get() << ".\n";

    // This doesn't work: Getter::value is private
    win.width.value = 640;
    win.height.value = 480;
}

Это выглядит справедливо для меня, и я мог бы даже переопределить логику get, используя некоторые другие приемы частичной специализации шаблонов. То же самое можно применить к какому-либо шаблону классов Setter или даже к классам GetterSetter.

Что ты думаешь?

Ответы [ 8 ]

4 голосов
/ 07 января 2010

Хотя решение является точным с точки зрения реализации, архитектурно оно находится на полпути. Смысл паттерна Getter / Setter состоит в том, чтобы дать классу контроль над его данными и уменьшить сцепление (то есть другой класс знает, как хранятся данные ). Это решение достигает первого, но не совсем второго.

Фактически другой класс теперь должен знать две вещи - имя переменной и метод в получателе (то есть .get()) вместо одного - например, getWidth(). Это вызывает увеличение сцепления.

Сказав все это, это раскалывает пресловутые архитектурные волосы. В конце дня это не имеет большого значения.

РЕДАКТИРОВАТЬ ОК, теперь для дерьма и хихиканья, вот версия метода получения с использованием операторов, поэтому вам не нужно делать .value или .get()

template <class T>
struct TemplateParameterIndirection // This hack works for MinGW's GCC 4.4.1, dunno others
{
    typedef T Type;
};

template <typename T,class Owner>
class Getter
{
public:
    friend TemplateParameterIndirection<Owner>::Type; // Befriends template parameter

    operator T()
    {
        return value;
    }

protected:
    T value;

    T& operator=( T other )
    {
       value = other;
       return value;  
    }


};

class Window
{
public:
    Getter<int,Window> _width;
    Getter<int,Window> _height;

    void resize(int width,int height)
    {
        // do actual window resizing logic
        _width = width; //using the operator
        _height = height; //using the operator
    }
};

void someExternalFunction()
{
    Window win;

    win.resize(640,480); // Ok: public method
    int w2 = win._width; //using the operator
    //win._height = 480; //KABOOM
}

РЕДАКТИРОВАТЬ Исправлен жестко заданный оператор присваивания. Это должно работать достаточно хорошо, если у самого типа есть оператор присваивания. По умолчанию структуры имеют таковые, поэтому для простых это должно работать из коробки.

Для более сложных классов вам потребуется реализовать оператор присваивания, который достаточно справедлив. С RVO и Copy On Write оптимизациями это должно быть достаточно эффективным во время выполнения.

1 голос
/ 07 января 2010

Поскольку Игорь Зевака опубликовал одну версию, я выложу ту, которую написал давно. Это немного отличается - в то время я наблюдал, что большинство реальное использование пар get / set (которые фактически что-то делали) было для обеспечения значения переменной, находящейся в заранее определенном диапазоне. Это немного более обширно, например, добавление операторов ввода / вывода, где экстрактор все еще применяет определенный диапазон. Он также содержит небольшой код теста / упражнения, чтобы показать общее представление о том, что он делает и как он это делает:

#include <exception>
#include <iostream>
#include <functional>

template <class T, class less=std::less<T> >
class bounded {
    const T lower_, upper_;
    T val_;

    bool check(T const &value) {
        return less()(value, lower_) || less()(upper_, value);
    }

    void assign(T const &value) {
        if (check(value))
            throw std::domain_error("Out of Range");
        val_ = value;
    }

public:
    bounded(T const &lower, T const &upper) 
        : lower_(lower), upper_(upper) {}

    bounded(bounded const &init) 
        : lower_(init.lower), upper_(init.upper)
    { 
        assign(init); 
    }

    bounded &operator=(T const &v) { assign(v);  return *this; }

    operator T() const { return val_; }

    friend std::istream &operator>>(std::istream &is, bounded &b) {
        T temp;
        is >> temp;

        if (b.check(temp))
            is.setstate(std::ios::failbit);
        else
            b.val_ = temp;
        return is;
    }
};


#ifdef TEST

#include <iostream>
#include <sstream>

int main() {
    bounded<int> x(0, 512);

    try {
        x = 21;
        std::cout << x << std::endl;

        x = 1024;
        std::cout << x << std::endl;
    }

    catch(std::domain_error &e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    std::stringstream input("1 2048");
    while (input>>x)
        std::cout << x << std::endl; 

    return 0;
}

#endif
1 голос
/ 07 января 2010

FWIW вот мои мнения по вашим вопросам:

  1. Обычно дело в том, что в установщике применяются бизнес-логика или другие ограничения. Вы также можете вычислить или вычислить виртуальные переменные, отсоединив переменную экземпляра с помощью методов доступа.
  2. Не то, что я знаю. Проекты, над которыми я работал, имели семейство макросов C, чтобы исключить такие методы
  3. Да; Я думаю, что это довольно опрятно. Я просто волнуюсь, что это не стоит хлопот, это просто запутает других разработчиков (еще одна концепция, которую им нужно втиснуть в голову), и не сильно экономит, исключая такие методы вручную.
0 голосов
/ 08 января 2010

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

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

Вместо того, чтобы искать более простые способы написания car.getFrontLeftWheel(), спросите себя, зачем пользователю класса вообще понадобится переднее левое колесо. Вы обычно манипулируете этим колесом непосредственно во время вождения? Автомобиль должен позаботиться обо всем, что крутит колеса, не так ли?

0 голосов
/ 07 января 2010

А теперь вопрос, а что если вам понадобится также setter.

Я не знаю о вас, но у меня есть (примерно) два типа классов:

  • класс для логики
  • сгустки

BLOB-объекты - это просто свободные коллекции всех свойств бизнес-объекта. Например, Person будет иметь surname, firstname, несколько адресов, несколько профессий ... поэтому Person может не иметь логики.

Для больших двоичных объектов я обычно использую канонический приватный атрибут + getter + setter, поскольку он абстрагирует фактическую реализацию от клиента.

Однако, хотя ваш шаблон (и его развитие Игоря Зевека) действительно хороши, они не решают проблему настройки и не решают бинарную совместимость проблемы.

Полагаю, я бы прибегнул к макросам ...

Что-то вроде:

// Interface
// Not how DEFINE does not repeat the type ;)
#define DECLARE_VALUE(Object, Type, Name, Seq) **Black Magic Here**
#define DEFINE_VALUE(Object, Name, Seq) ** Black Magic Here**

// Obvious macros
#define DECLARE_VALUER_GETTER(Type, Name, Seq)\
   public: boost::call_traits<Type>::const_reference Name() const

#define DEFINE_VALUE_GETTER(Object, Name)\
   boost::call_traits<Name##_type>::const_reference Object::Name ()const\
   { return m_##Name; }

#define DECLARE_VALUE_SETTER(Object, Type, Name)\
   public: Type& Name();\
   public: Object& Name(boost::call_traits<Type>::param_type i);

#define DEFINE_VALUE_SETTER(Object, Name)\
   Name##_type& Object::Name() { return m_##Name; }\
   Object& Object::Name(boost::call_traits<Name##_type>::param_type i)\
   { m_##Name = i; return *this; }

Что будет использоваться как:

// window.h
DECLARE_VALUE(Window, int, width, (GETTER)(SETTER));

// window.cpp
DEFINE_VALUE(Window, width, (GETTER)); // setter needs a bit of logic

Window& Window::width(int i) // Always seems a waste not to return anything!
{ 
  if (i < 0) throw std::logic_error();
  m_width = i;
  return *this;
} // Window::width

С небольшим количеством препроцессорной магии это будет работать очень хорошо!

#include <boost/preprocessor/seq/for_each.hpp>
#include <boost/preprocessor/tuple/rem.hpp>

#define DECLARE_VALUE_ITER(r, data, elem)\
  DECLARE_VALUE_##elem ( BOOST_PP_TUPLE_REM(3)(data) )

#define DEFINE_VALUE_ITER(r, data, elem)\
  DEFINE_VALUE_##elem ( BOOST_PP_TUPLE_REM(2)(data) )

#define DECLARE_VALUE(Object, Type, Name, Seq)\
   public: typedef Type Name##_type;\
   private: Type m_##Name;\
   BOOST_PP_SEQ_FOREACH(DECLARE_VALUE_ITER, (Object, Type, Name), Seq)

#define DEFINE_VALUE(Object, Name, Seq)\
   BOOST_PP_SEQ_FOREACH(DEFINE_VALUE_ITER, (Object, Name), Seq)

Ладно, не типа safe, и все, но:

  • это разумный набор макросов, я думаю
  • это просто в использовании, пользователю когда-либо придется беспокоиться только о 2 макросах, хотя, как и в случае с шаблонами, ошибки могут стать проблемными
  • использование boost.call_traits для эффективности (выбор констант и / или значений)
  • там больше функциональности: дуэт геттера / сеттера

  • это, к сожалению, набор макросов ... и не будет жаловаться, если вы когда-либо

  • это наносит ущерб методам доступа (общедоступным, защищенным, приватным), поэтому лучше не перемежать их с классом

Вот канонический пример:

class Window
{
  // Best get done with it
  DECLARE_VALUE(Window, int, width, (GETTER));
  DECLARE_VALUE(Window, int, height, (GETTER));

// don't know which is the current access level, so better define it
public:

};
0 голосов
/ 07 января 2010

Это может быть излишним в этом случае, но вы должны проверить идиому адвоката / клиента для разумного использования дружбы Прежде чем найти эту идиому, я вообще избегал дружбы.

http://www.ddj.com/cpp/184402053/

0 голосов
/ 07 января 2010

Здесь я думаю, #define все еще полезны.

Версия шаблона сложна и трудна для понимания - определенная версия очевидна

#define Getter(t, n)\
     t n;\
     t get_##n() { return n; }

class Window
{
    Getter(int, height);
}

Я уверен, что у меня немного неправильный синтаксис - но вы поняли.

Если бы в, скажем, Boost был хорошо известный набор шаблонов, я бы использовал их. Но я бы не стал писать свой.

0 голосов
/ 07 января 2010
  1. Вы также можете использовать метод типа получения или установки для получения или установки вычислимых значений, во многом так же, как свойства используются в других языках, таких как C #

  2. Я не могу придумать разумного способа абстрагировать получение и установку неизвестного числа значений / свойств.

  3. Я недостаточно знаком со стандартом C ++ ox, чтобы комментировать.

...