суббота, 28 июня 2025 г.

[prog.c++.dreams] В очередной раз выяснил, что в C++ using это не strong typedef :(

В С++ есть отличная штука под названием using. Позволяет вводить удобные псевдонимы для сложночитаемых имен типов или же прикрывать тонкой завесой детали реализации.

Но, к сожалению, using не может сделать совершенно новый тип. Поэтому если у вас есть что-то вроде:

using type_a = ...;
using type_b = ...;

void do_something(type_a v) {...}
void do_something(type_b v) {...}

То вы внезапно можете обнаружить, что ваш код перестал компилироваться потому, что type_a и type_b оказались синонимами для одного и того же типа.

Например, раньше было:

using small_data = std::map<some_key, some_value>;
using large_data = std::unordered_map<special_key, some_value>;

using type_a = small_data::iterator;
using type_b = large_data::iterator;

А в один прекрасный момент стало:

using special_key = some_key;

using small_data = std::map<some_key, some_value>;
using large_data = std::map<special_key, some_value>;

И все 🙁
Типы type_a и type_b оказались одинаковыми.

Недавно в очередной раз наступил на подобные грабли, но немного в другом контексте. Было что-то вроде:

namespace processing
{

class processor {...};

template<typename Data>
void
handle(const processor & how, const Data & what)
{
  // Должна быть найдена подходящая функция за счет ADL.
  apply(what, how);
}

// namespace processing

namespace data_type_a
{

struct data {...};

void apply(const data & what, const processing::processor & how) {...}

// namespace data_type_a

namespace data_type_b
{

struct data {...};

void apply(const data & what, const processing::processor & how) {...}

// namespace data_type_b

И т.д.

Т.е. смысл в том, что в конкретном пространстве имен data_type_X должна быть функция apply, который компилятор посредством ADL находит для вызова внутри processing::process.

Все шло хорошо до момента, пока не появились data_type_i и data_type_j, в которых тип data был определен через using:

namespace data_type_i
{

using data = std::map<...>;

void apply(const data & what, const processing::processor & how) {...}

// namespace data_type_i

namespace data_type_j
{

using data = std::vector<...>;

void apply(const data & what, const processing::processor & how) {...}

// namespace data_type_j

И вот когда эти типы начали отдавать в processing::process, то код перестал компилироваться. Причем далеко не сразу удалось понять, почему ни одна из определенных в правильных пространствах имен apply не выбиралась компилятором как подходящая.

А дело в том, что если заменить псевдонимы, то получались функции вида:

void apply(const std::map<...> &, const processing::processor&);
void apply(const std::vector<...> &, const processing::processor&);

И естественно, что ADL не мог их найти ни в пространстве имен std, ни в processing.

Было больно, т.к. пришлось отказаться от очень красивого способа привязки специфических данных к их обработчику 😪

В очередной раз захотелось, чтобы using в С++ мог работать как strong typedef. Чтобы можно было написать что-то вроде:

namespace data_type_i
{

using(new) data = std::map<int, std::string>;

// namespace data_type_i

И чтобы компилятор начал считать, что data и std::map<int, std::string> теперь разные типы. И что тип data теперь принадлежит пространству имен data_type_i, а не std.

PS. В свете добавления в C++ рефлексии может оказаться, что наколхозить какой-то нестандартый strong_typedef_for, типа:

namespace data_type_i
{

using data = my::strong_typedef_for< std::map<int, std::string> >;

// namespace data_type_i

через рефлексию будет быстрее и проще, чем дождаться появления using(new) в стандарте. Обычная традиция C++: если что-то можно собрать своими руками дендро-фекальным методом, то включать в стандарт удобный и нормальный вариант никто не будет.

2 комментария:

Stanislav Mischenko комментирует...

А в чём проблема сделать "дедовским" способом? То есть так:

namespace data_type_i
{
struct data { const std::map<...>& map; };

void apply(const data & what, const processing::processor & how) {...}
}

и вызывать соответственно

processing::processor;
map<...> m;

processing::handle(processor, data_type_i::data{m});

Не элегантно, но работает.

eao197 комментирует...

Все это вызывалось внутри шаблонов, и эти шаблоны не были расчитаны на то, что data нужно еще во что-то оборачивать.