angle-left

Properties в C++

Доброе утро!

В данной статье я покажу, как в C++ можно реализовать properties, подобно тому, как это сделано в C# (но с чуть меньшим сахарком).

Для начала, поймём, что же это. Properties (они же свойства) — это такие поля, обращение к которым идёт через неявный вызов геттеров и сеттеров. То есть, эдакий синтаксический сахар. Мне он не нравится как минимум по причине неявного вызова функции за синтаксисом обращения к полю, но реализовать такую функциональность на C++ захотелось попробовать.

Теперь к реализации. Идея простая. Создадим класс, от которого все эти свойства будут наследоваться. В нём добавим виртуальные методы set и get, по-умолчанию определённые такими, какими их и привычно видеть, а конструкторы, операторы присваивания и операторы работы с потоками ввода-вывода определим через них. Получается следующая милота:

#include <iostream>


template <typename T>
struct Property {
protected:
    T value;
public:
    Property() = default;

    Property(const T &value) : Property() {
        set(value);
    }

    const T & operator=(const T &value) {
        set(value);
        return this->value;
    }

    operator T() const {
        return get();
    }

    virtual void set(const T &value) {
        this->value = value;
    }

    virtual T get() const {
        return value;
    }
};

template <typename T>
std::ostream& operator<<(std::ostream &os, const Property<T> &prop) {
    os << static_cast<T>(prop);
    return os;
}

template <typename T>
std::istream& operator>>(std::istream &is, Property<T> &prop) {
    T val;
    is >> val;
    prop = val;
    return is;
}

Теперь мы можем создавать внутренние классы, сразу же объявляя поля, и перегружая методы на своё усмотрение. В геттере и сеттере получаем доступ к this->value, а в сеттере есть аргумент value, почти как в родном шарпе. Ну и добавим макросов (куда ж без них?), чтобы жилось совсем красиво:

#define PROPERTY_S(name, type, setter)               \
    struct name##name : Property<type> {             \
        name##name() = default;                      \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        void set(const type &value) setter           \
    } name

#define PROPERTY_SD(name, type, defval, setter)      \
    struct name##name : Property<type> {             \
        name##name() : Property(defval) {}           \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        void set(const type &value) setter           \
    } name

#define PROPERTY_G(name, type, getter)               \
    struct name##name : Property<type> {             \
        name##name() = default;                      \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        int get() const getter                       \
    } name

#define PROPERTY_GD(name, type, defval, getter)      \
    struct name##name : Property<type> {             \
        name##name() : Property(defval) {}           \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        int get() const getter                       \
    } name

#define PROPERTY_D(name, type, defval, setter, getter) \
    struct name##name : Property<type> {               \
        name##name() : Property(defval) {}             \
        name##name(type value) : name##name() {        \
            set(value);                                \
        }                                              \
        void set(const type &value) setter             \
        int get() const getter                         \
    } name

#define PROPERTY(name, type, setter, getter)         \
    struct name##name : Property<type> {             \
        name##name() = default;                      \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        void set(const type &value) setter           \
        int get() const getter                       \
    } name

Накидали макросов на все случаи жизни. Использование довольно просто: name — итоговое имя свойства (лучше не дублировать), type — тип хранимого значения, defval — умолчательное значение, с которым оно будет сконструировано, а getter и setter — блоки кода, тела соответствующих функций.

Добавлю компилирующийся пример, чтобы лучше видеть, как это работает:

#include <iostream>


template <typename T>
struct Property {
protected:
    T value;
public:
    Property() = default;

    Property(const T &value) : Property() {
        set(value);
    }

    const T & operator=(const T &value) {
        set(value);
        return this->value;
    }

    operator T() const {
        return get();
    }

    virtual void set(const T &value) {
        this->value = value;
    }

    virtual T get() const {
        return value;
    }
};

template <typename T>
std::ostream& operator<<(std::ostream &os, const Property<T> &prop) {
    os << static_cast<T>(prop);
    return os;
}

template <typename T>
std::istream& operator>>(std::istream &is, Property<T> &prop) {
    T val;
    is >> val;
    prop = val;
    return is;
}

#define PROPERTY_S(name, type, setter)               \
    struct name##name : Property<type> {             \
        name##name() = default;                      \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        void set(const type &value) setter           \
    } name

#define PROPERTY_SD(name, type, defval, setter)      \
    struct name##name : Property<type> {             \
        name##name() : Property(defval) {}           \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        void set(const type &value) setter           \
    } name

#define PROPERTY_G(name, type, getter)               \
    struct name##name : Property<type> {             \
        name##name() = default;                      \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        int get() const getter                       \
    } name

#define PROPERTY_GD(name, type, defval, getter)      \
    struct name##name : Property<type> {             \
        name##name() : Property(defval) {}           \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        int get() const getter                       \
    } name

#define PROPERTY_D(name, type, defval, setter, getter) \
    struct name##name : Property<type> {               \
        name##name() : Property(defval) {}             \
        name##name(type value) : name##name() {        \
            set(value);                                \
        }                                              \
        void set(const type &value) setter             \
        int get() const getter                         \
    } name

#define PROPERTY(name, type, setter, getter)         \
    struct name##name : Property<type> {             \
        name##name() = default;                      \
        name##name(type value) : name##name() {      \
            set(value);                              \
        }                                            \
        void set(const type &value) setter           \
        int get() const getter                       \
    } name




                             // \\ // \\
                             //EXAMPLE//
                             // \\ // \\

class A {
public:
    PROPERTY(val, int, { this->value = 2*value; }, { return 3*value; });
};


int main() {
    A obj;
    std::cin >> obj.val; // Enter 2 (stored 2*2=4)
    std::cerr << obj.val << std::endl; // See 3*4=12

    int a = obj.val; // Get 3*4=12
    std::cerr << a << std::endl; // See 12

    obj.val = a; // Store 2*12=24
    std::cerr << obj.val; // See 3*24=72

    return 0;
}

Прямо как в C#!

Вот мы и завезли properties в C++. Было бы интересно посмотреть на реализацию подобного механизма в какой-нибудь Java, если кто-то отважится попробовать.