Пишу свой форматтер. Он состоит из двух классов: Argument и Format.
Format принимает неопределённое количество параметров типа Argument.
Argument принимает в конструктор разные типы и конвертирует их во внутренний std::wstring m_strValue. Тем самым, он играет две роли: 1) фильтр, который явно описывает допустимые типы, 2) суб-форматтер, которые умеет знаковые целые превращать в строки десятичного формата, беззнаковые — в строки с префиксом "0x", узкие и широкие строки приводит к одному виду (std::wstring) и т.д.
class Argument
{
Argument(const std::wstring& ws);
Argument(const wchar_t* ps);
Argument(int i);
Argument(unsigned int i);
//...
};
А Format делает так:
template<typename T>
concept ConvertibleToArgument = std::convertible_to<T, Argument>;
class Format
{
//...
template<ConvertibleToArgument... Args>
static void Output(const Args&... messageParts)
{
std::initializer_list<Argument> partList { messageParts... };
Impl(partList);
}
//...
};
concept ConvertibleToArgument проверяет, что переданный аргумент можно сконвертировать в Argument. А Impl далее склеивает подстроки.
Всё хорошо, всё работает. Но потом мне надоела простыня конструкторов в Argument, и я решил сгруппировать их по имплементации. Т.е. сделать шаблонными.
class Argument
{
public:
// Constructor from a wide string.
template <typename T_WideString> requires
(
std::is_same_v<T_WideString, const wchar_t*> ||
std::is_same_v<T_WideString, const std::wstring&>
)
Argument(T_WideString wideString)
{
m_strValue = wideString;
}
//...
}
Теперь при вызове
Format::Output(L"Test"); компилятор не может найти подходящей перегрузки.
Вот болт:
https://gcc.godbolt.org/z/hY3ePzvE6
Если совсем убрать
concept ConvertibleToArgument, а функцию Output объявить так:
template<typename... Args>
static void Output(const Args&... messageParts)
То всё снова работает, но если передать неизвестный аргумент, ошибка компиляции показывает не на вызов с этим аргументом, а внутрь функции. Добавление static_assert ситуацию не улучшает:
static_assert((... && requires (const Args& arg) { Argument(arg); }),
"Arguments must be convertible to Argument");
(Пока я вообще так и не понял, в каких ситуациях полезен static_assert из-за этого свойства показывать не источник ошибки, а внутрь реализации).
Как совместить concept, который позволяет подсветить неверные вызовы, с шаблонными конструкторами Argument?
Почему с явно описанным конструктором:
Argument(const wchar_t* s) { m_strValue = s; }
всё работает, а с шаблонным конструктором и проверкой
std::is_same_v<T_WideString, const wchar_t*> — нет?