strong typedef

ya.imdex
ya.imdex

Рассмотрим простой пример:

using UserId = int;
using RoleId = int;

option<User> findUserById(UserId id);
option<Role> findRoleById(RoleId id);

auto userId = UserId(0);
auto roleId = RoleId(1);

findUserById(roleId); // oops
findRoleById(userId); // oops

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

using UserId = new int;
using RoleId = new int;

option<User> findUserById(UserId id);

findUserById(RoleId(1)); // compile error
findUserById(0); // compile error
findUserById(UserId(0)); // Ok
18
рейтинг
25 комментариев
yndx-antoshkka
Недавно появился вот такой трюк:

enum class UserId: int {};
enum class RoleId: int{};

Достаточно ли его для ваших нужд?
yndx-antoshkka
ru.night.beast
yndx-antoshkka, а если тип не интегральный? например UUID
ru.night.beast
ya.imdex
yndx-antoshkka, это что-то из разряда #define private public, конечно можно извращаться, но зачем? И, как заметил ru.night.beast, работать это будет для очень ограниченного числа типов.
ya.imdex
smertigdon
yndx-antoshkka, по-моему, самым элегантным решением проблемы будет разрешение писать код:
enum class UserId : AnyType {};
smertigdon
develoit
зачем плодить лишние сущности из-за своей лени или глупости?

struct UserId { int value; };
struct RoleId { int value; };

option<User> findUserById(UserId id);

findUserById(RoleId{1}); // compile error
findUserById(0); // compile error
findUserById(UserId{42}); // Ok
develoit
Andrey Davydov
develoit, это совсем не то же самое. Для Ваших структур UserId, RoleId не специализирован std::hash, не определены операторы сравнения и куча других полезных операций доступных для оборачиваемого типа.
Andrey Davydov
ya.imdex
develoit, пример с int вышел не очень удачным. Если взять std::string, то получится, что каждый метод и оператор нужно продублировать. Поэтому и хочется чтобы весь этот бойлерплейт взял на себя компилятор.
ya.imdex
ru.night.beast
ya.imdex, неявное преобразование к базовому типу (как при наследовании) нужно ?
если нет, то как быть со всеми функциями, определенными вне класса?
ru.night.beast
ya.imdex
ru.night.beast, неявное преобразование безусловно необходимо, но есть тонкий момент: оно не будет работать для вызовов с использованием ADL, т.к. новый тип может быть объявлен в другом пространстве имен. Эту проблему можно решить заставив компилятор также выполнять ADL для исходного типа (с использованием неявного преобразования) в случае неудачи для конкретного типа.
ya.imdex
ru.night.beast
ya.imdex, если для реализации использовать механизмы наследования, то адл будет работать.
ru.night.beast
ya.imdex
ru.night.beast, наследование не будет работать для final типов.
ya.imdex
ru.night.beast
ya.imdex, это если пытаться своими силами делать. у компилятора же таких ограничений нет.
ru.night.beast
ya.imdex
ru.night.beast, если говорить о деталях реализации, то компилятор может и не создавать новый тип, а просто использовать исходный.
ya.imdex
Павел Корозевцев
Зачем нагромождать синтаксис языка, если можно просто включить такую тривиальную проверку в (локальные) Guidelines?
Павел Корозевцев
ya.imdex
Павел Корозевцев, какую именно проверку?
ya.imdex
Павел Корозевцев
ya.imdex, извиняюсь. Подумал чуть больше об этом и понял, что не такая уж и тривиальная получается. Но всё-таки теоретически можно попросить clang-tidy проверить, не пытаемся ли мы использовать переменную, объявленную как UserId, использовать в качестве RoleId. И тут речь не о приведении типа в общем вида, а именно о приведении между типами, лежащими под определениями typedef'ов.
Вообще, я бы хотел донести мысль о том, что некоторые предложения, обсуждаемые на этом сайте, вполне могут быть реализованы с использованием инструментов вроде clang-tidy. Вовсе не нужно придумывать proposal для каждой новой фичи. Особенно это касается вот таких compile-time "проверок от дураков". Компилятор не должен всё делать за нас.
Павел Корозевцев
Павел Корозевцев
А вот в Guidelines такое предложить было бы уместнее, имхо.
Павел Корозевцев
ya.imdex
Павел Корозевцев, для всяких статических анализаторов все равно нужно будет как-то указывать: строгий typedef или нет. К тому же статических анализаторов много, кто-то использует один, кто-то другой, и навязывать один конкретный - плохая идея.
>Компилятор не должен всё делать за нас.
Это и есть основная задача компилятора - делать за разработчика грязную работу, а особенно компилятора языка со статической типизацией.

>А вот в Guidelines такое предложить было бы уместнее, имхо.
Как это спасет разработчика от ошибок? Также как и, например, рекомендация не разыменовывать нулевой указатель - никак.
ya.imdex
ru.night.beast
Павел Корозевцев> Вовсе не нужно придумывать proposal для каждой новой фичи. Особенно это касается вот таких compile-time "проверок от дураков". Компилятор не должен всё делать за нас.

вообще то проверка типов -- это как раз область ответственности компилятора а не кодинг стандартов. к тому же, это не просто compile-time проверка. должен быть создан свой тип, чтобы линкер мог отличить функции использующие UserId от RoleId.
ru.night.beast
mrgordonfreman
Не вижу смысла усложнять язык избыточными сущностями. Эта задача достаточно просто решается следующим образом

Один раз описываем класс-обертку со всеми необходимыми конструкторами и операторами сравнения, примерно так

template<typename Tag, typename T>
class strong_type {
T data_;
public:
// ...
template<typename U>
explicit strong_type(U&& u): data_(std::forward<U>(u)) {}
// ...
operator T () const noexcept { return data_; }
// ...
};

И дальше использовать таким образом

using UserId = strong_type<class UserIdTag, int>;
using RoleId = strong_type<class RoleIdTag, int>;

void foo(UserId id);
void bar(RoleId id);

foo(10); // compilation error
foo(UserId{10});
foo(RoleId{10}); // compilation error
mrgordonfreman
ya.imdex
mrgordonfreman,
using Id = strong_type<class IdTag, std::string>;
Id id("blah");
id.empty(); // ошибка, а хочется иметь весь набор методов
ya.imdex
mrgordonfreman
ya.imdex,
Добавим в класс более умные операторы приведения типов

operator T const& () const noexcept { return data_; }
operator T& () noexcept { return data_; }

И тогда через приведение к ссылке на строку получаем все методы

using Id = strong_type<class IdTag, std::string>;
Id id("blah");
static_cast<std::string&>(id).empty(); // now it's ok

Не обязательно же для каждого вызова метода выполнять приведение типов, достаточно в начале функции получить ссылку на значение и работать уже с ней. Основная задача - получение строго интерфейса - решена.
Если нужно больше сахара, то можно перегрузить оператор ->

T* operator -> () noexcept { return &data_; }
T const* operator -> () const noexcept { return &data_; }

Id id("blah");
id->empty(); // now it's ok

А если нужно еще больше сахара, то есть пропозал на перегрузку dot operator
open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4173.pdf
Все, что можно решить в бибилотеках, должно быть решено в бибилотеках.
mrgordonfreman
ViTech
mrgordonfreman, т.е. какие-то Tag'и ещё таскать нужно?

Есть подозрения, что определять все возможные конструкторы для всех возможных классов, оборачиваемых в strong_type, будет затруднительно, мягко говоря. Как будет выглядеть strong_type, который бы нормально тот же std::string обернул? Чтобы хотя бы такой код работал:

#include <string>
#include <iostream>

using namespace std;

#if 0
using UserId = string;
using RoleId = string;
#else
using UserId = strong_type< class UserIdTag, string >;
using RoleId = strong_type< class RoleIdTag, string >;
#endif

void printUserId( const UserId & id )
{ cout << "UserId = " << id << endl; }

void printRoleId( const RoleId & id )
{ cout << "RoleId = " << id << endl; }

int main (int, char * [])
{
UserId user_id = "UserId";
RoleId role_id = "RoleId";

printUserId( user_id );
printRoleId( role_id );

return 0;
}

Ещё нужно учитывать, что одни классы имеют конструктор по умолчанию, а другие не имеют. В некоторых классах определённые конструкторы удалены. Кроме конструкторов надо ещё операторы присваивания писать, и.т.п.
ViTech
ilnur.khuziev
Мне кажется вы хотите operator. (оператор точка) который сейчас прорабатывается и к сожалению не вошёл в cpp17
ilnur.khuziev
ViTech
Если новый тип обладает всеми свойствами исходного типа, то это больше всего похоже на наследование, чем на какую-то обёртку с композицией. Думаю, в этом направлении и надо решать задачу. На текущий момент реализовать можно как-то так:

class UserId : public string
{
public:
using string::string;
using string::operator=;
// using всё остальное;
};
class RoleId : public string
{
public:
using string::string;
using string::operator=;
// using всё остальное;
};

Но каждый раз такую простыню расписывать не интересно. Суть остаётся одна: указать родительский класс и по умолчанию использовать все его конструкторы, операторы и т.п, :

class UserId : public string {using all};
class RoleId : public string {using all};

Ключевые слова typedef и using используются для объявления псевдонимов. Так лучше и оставить, чтобы не вносить путаницу, где псевдоним, а где новый тип. Новые типы объявляются с помощью class/struct и им подобным. Тогда форма объявления может быть какой-нибудь такой:

class UserId : using string;
class RoleId : using string;

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