Шаблонный CV-квалификатор

xjossy
xjossy

Формулировка проблемы

Сейчас приходится дублировать код при наличии const и не-const версии одного метода в классе. Типичный пример - геттер:

class T;

class A {
public:
  T& getT() {
    return t;
  }

  const T& getT() const {
    return t;
  }

private:
  T t;
};

Если логика этой функции более сложна, то возникает нежелательное дублирование кода. Элегантного метода решения на данный момент не существует. Скотт Мейерс рекомендует использовать const_cast, что не способствует читаемости и надёжности кода. В современном Стандарте его подход реализуется так:

class T;

class A {
public:
  T& getT() {
    //something complicated
  }

  const T& getT() const {
    return const_cast<T&>(std::as_const(*this).getT());
  }

private:
  T t;
};

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

class T;

class A {
  template<class ARef>
  static auto& getTImpl(ARef* pthis) {
    //something compicated
  }
public:
  T& getT() {
    return getTImpl(this);
  }

  const T& getT() const {
    return getTImpl(this);
  }

private:
  T t;
};

Предлагаемое решение

Добавить новый тип неклассовых параметров шаблона - qualifier, который может применяться к функции. Пример кода:

class T;

class A {
public:
  template<qualifier cv>
  cv<T>& getT() cv {
    //something compicated
  }

private:
  T t;
};

Это полностью решит проблему.

Минусы этого решения:

1. Необходимо ввести новое ключевое слово qualifier, причём глобальное, так как в этом контексте может появится любое имя скалярного типа. В качестве решения проблемы можно создать enum-тип std::qualifier, наделённый дополнительными возможностями:
* его значение (как шаблонного параметра) может выводиться из квалификатора this
* он может применяться к типу, наделяя его соотв. квалификаторами. Это может быть сделано без вмешательства в core: std::apply_qualifier<cv, T> (вместо cv<T> в примере)
* constexpr значение типа std::qualifier может применяться к функциям наравне с const-volatile квалификаторами.

Такое решение позволит обойтись без дополнительных ключевых слов.

2. Необходимо проработать использования volatile-квалификатора, так как пример выше подразумевает наличие volatile и const volatile специализации. Не ясно, нужны ли они. В качестве решения предлагается возможность ограничивать квалификатор только одним введением дополнительных типов: std::const_qualifier, std::volatile_qualifier.

3. Необходимо убедиться в том, что добавление идентификатора на местро квалификатора не вызовет синтаксических неоднозначностей.

С учётом всех этих замечаний, решение могло бы быть таким:

class T;

class A {
public:
  template<std::const_qualifier cv>
  std::apply_qualifier<cv, T&> getT() cv {
    //std::apply_qualifier<cv, T&> в возвращаемом типе обычно можно заменить на auto
    //something compicated
  }

private:
  T t;
};

Заметка о реализации

Наиболее существенной проблемой в реализации будет скорее всего вывод значения квалификатора, как шаблонного параметра. Это можно разрешить, так как thiscall функция содержит неявный параметр this, из типа которого могут быть выведены квалификаторы (даже в рамках современного Стандарта).

Допольнительная мотивация - ещё один пример применения

Рассмотрим class SerializableScheme, который позволяет сериалиовывать и десериализовывать некоторое количество данных. Этот класс может использовать указатели, чтобы сразу помещать данные в нужное место. Его интерфейс может быть таким:

class SerializableScheme {
public:
  template<class T>
  void addData(T& data);

  std::string save() const;
  void load(const std::string& saved);
};

Его использование может быть таким:

class A {
public:
  SerializableScheme getScheme() {
    SerializableScheme res;

    res.addData(a);
    res.addData(b);
    res.addData(c);

    return res;
  }

private:
  int a, b, c;
};

//использование:
A a;

//сохранение:
std::ofstream("a.txt") << a.getScheme().save();

//загрузка
std::string saved;
std::ifstream("a.txt") >> saved;
a.getScheme().load(saved);

Проблема в том, что с помощью такого сериализатора не получится сохранить констанстный объект A.

Решение - сделать getScheme() константным методом не сработает, т. к. для объект, полученный из констрантного объекта A не сможет реализовать метод load() (без применения const_cast).

Остаётся только такое решение:

class A {
public:
  SerializableScheme getScheme() {
    SerializableScheme res;

    res.addData(a);
    res.addData(b);
    res.addData(c);

    return res;
  }

  ConstSerializableScheme getScheme() const {
    ConstSerializableScheme res;

    res.addData(a);
    res.addData(b);
    res.addData(c);

    return res;
  }

private:
  int a, b, c;
};

Если объект A сложнее, то дублирование кода будет более существенным.

С введением параметра квалификатора решение было бы таким:

template<std::const_qualifier cv>
using SerializableSchemeT = ...; // SerializableScheme для пустого cv, ConstSerializableScheme для константного cv

class A {
public:
  template<std::const_qualifier cv>
  SerializableSchemeT<cv> getScheme() cv {
    SerializableSchemeT<cv> res;

    res.addData(a);
    res.addData(b);
    res.addData(c);

    return res;
  }

private:
  int a, b, c;
};

 

Спасибо всем, кто дочитал до конца, надеюсь, идея понравилась

0
рейтинг
1 комментарий
Andrey Davydov

На этом сайте предлагалось как минимум трижды:

1. https://stdcpp.ru/proposals/ca25e663-71b4-45da-a8ab-9ed7deb17c6d

2. https://stdcpp.ru/proposals/b02ce2fb-5e59-4f15-b21c-a2cb8e25aaa0

3. https://stdcpp.ru/proposals/b41bac7b-f618-4ae0-b0fa-c3744ba26b9b

Актуальный proposal: http://wg21.link/p0847

Andrey Davydov
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).
Все предложения