В чём различие между std::remove_cvref_t и std::decay_t при нормализации типа шаблонного аргумента?
std::remove_cvref_t удаляет только ссылочность и квалификаторы const/volatile, сохраняя массивы и функции их исходными типами. std::decay_t дополнительно применяет стандартные преобразования передачи по значению: массив превращает в указатель, функцию — в указатель на функцию.
Поэтому для сохранения структуры исходного типа используют remove_cvref_t, а для имитации хранения значения или передачи аргумента по значению — decay_t.
В шаблонном коде тип параметра часто выводится с учётом ссылочности и cv-квалификаторов. Например, forwarding-ссылка позволяет принять lvalue, rvalue, массив или функцию без предварительного преобразования, но полученный тип не всегда подходит для хранения или дальнейшего метапрограммирования.
Ранее для такой нормализации широко применяли std::decay, поскольку он моделирует преобразования, происходящие при передаче аргумента по значению. В C++20 появился std::remove_cvref, когда потребовалась более точная операция без нежелательного распада массивов и функций.
Если бездумно заменить remove_cvref_t на decay_t, можно потерять информацию о размере массива или о том, что переданный объект имеет функциональный тип. Это приводит к другому типу поля, другой перегрузке или невозможности применить операции, рассчитанные на массив или функцию.
Обратная ошибка тоже опасна: если тип предназначен для независимого хранения значения, сохранение массива или функции может сделать объект неудобным для копирования, потребовать специальной обработки или вообще привести к некорректному дизайну интерфейса.
std::remove_cvref_t<T> эквивалентен последовательному удалению ссылки, а затем верхнеуровневых const и volatile. Он не выполняет преобразование массива в указатель и не превращает функциональный тип в указатель на функцию.
std::decay_t<T> сначала убирает ссылки и cv-квалификаторы, затем применяет правила, близкие к инициализации параметра, передаваемого по значению:
U[N] превращается в U*;Минимальная иллюстрация различия:
Для универсальных ссылок выбор обычно определяется целью. Если нужно сравнить или классифицировать фактический тип объекта, предпочтителен remove_cvref_t. Если нужно создать тип, пригодный для обычного хранения значения, часто уместен decay_t, хотя для владения строкой или диапазоном нередко требуется явный выбор типа вместо автоматического распада.
Важно, что decay_t не делает объект безопасным для хранения. Например, преобразование массива в int* не продлевает время жизни массива, а лишь сохраняет адрес. Поэтому при проектировании контейнера или обёртки нужно отдельно решить вопрос владения и времени жизни.
Обёртка принимает произвольный аргумент и формирует метаданные о его типе. Для массива в метаданных нужно сохранить размер, чтобы затем выполнить проверку границ. Использование decay_t превратит массив в указатель, после чего размер массива будет утрачен.
Вариант с decay_t прост и подходит, если обёртка действительно должна хранить значение в форме, близкой к передаче по значению. Его минус — распад массивов и функций, а также возможная потеря информации, нужной для анализа типа.
Вариант с remove_cvref_t сохраняет массив как массив и функцию как функцию. Он выбран, если обёртка анализирует исходный объект или строит ограничения на основе его точного типа; результатом становится корректная проверка размера массива. Если же обёртка должна владеть данными, одного remove_cvref_t недостаточно: потребуется, например, явное копирование массива в контейнер.
remove_cvref_t верхнеуровневый const?Нет. Он удаляет верхнеуровневые const и volatile, но не меняет квалификаторы, находящиеся глубже. Для типа const int* const относится к объекту int, на который указывает указатель, а не к самому указателю, поэтому remove_cvref_t<const int*> остаётся const int*.
Это важно отличать от типа int* const: здесь const относится к самому указателю и будет удалён. Нельзя рассуждать только по наличию слова const; нужно определить, какой уровень типа оно квалифицирует.
decay_t не решает проблему висячего указателя на массив?Потому что преобразование массива в указатель меняет только тип, но не время жизни объекта. Если массив был локальным или временным и обёртка сохранила полученный указатель, после завершения времени жизни массива указатель станет недействительным.
decay_t полезен для моделирования передачи по значению, но не является механизмом владения. Для безопасного хранения нужно скопировать элементы, использовать владеющий контейнер или гарантировать внешний срок жизни данных.
decay_t оправдано?Оно оправдано, когда интерфейс сознательно хочет получить самостоятельное значение в форме, соответствующей передаче параметра по значению. Например, тип callable-объекта можно нормализовать для хранения в собственной обёртке, если обёртка принимает копируемый или перемещаемый объект и не должна сохранять его массивную или функциональную форму.
Однако это решение должно соответствовать семантике владения. Для массива decay_t даст указатель, а не копию элементов, поэтому при необходимости владения массивом нужно выбрать std::array, std::vector или другой явный тип.