В чём заключается концепция проверки совместимости с шаблонным конструктором?

Пишу форматтер, реализованный двумя классами: Argument и Format.
  • 25 июня 2026 г. 18:01
Ответы на вопрос 2
Коротко — идея проверки совместимости с шаблонным конструктором: явно ограничить (заклинить) то, какие типы может принять шаблонный конструктор, чтобы он не перехватывал все подряд, не ломал семантику copy/move и не давал неожиданных привидений. Практически это делается через SFINAE (std::enable_if / type traits) в старых стандартах или через requires / concepts в C++20, и обычно вы проверяете, можно ли построить ваш тип Argument из переданного аргумента (std::is_constructible / std::is_convertible).

Ниже — причины, что именно проверять, и примеры.

1) Что обычно нужно проверить
- Можно ли построить Argument из данного T: std::is_constructible<Argument, T&&>.
  - is_constructible — проверяет, что конструкция допустима (включая explicit). is_convertible — проверяет, можно ли неявно преобразовать.
- Запретить шаблон захватывать копирующий/перемещающий конструктор Format: если шаблонный конструктор принимает один параметр, он может конфликтовать с copy-constructor (или hide перемещающий). Поэтому нужно SFINAE-выключить случай, когда единственный параметр — уже Format.
- Иногда нужно учесть std::initializer_list — если вы хотите поддержать/отключить list-initialization, это отдельный кейс.

2) Рекомендации
- Явно =default копирующие/перемещающие конструкторы/операторы, если нужен стандартный behaviour (пользовательский конструк-тор шаблонный считается user-declared и может подавить генерацию move).
- Проверять std::is_constructible<Argument, T&&> (или std::convertible_to в C++20, если нужна неявная конверсия).
- Для C++20 — писать requires с концептом (чисто и читаемо). Для C++11/14 — use std::enable_if_t.

3) Примеры

C++20 (рекомендую, если доступно)
```cpp
#include <type_traits>
#include <vector>
#include <utility>

struct Argument {
    template<typename T>
    explicit Argument(T&& v) { /* ... */ }
};

struct Format {
    std::vector<Argument> args;

    Format() = default;
    Format(Format const&) = default;
    Format(Format&&) = default;

    template<typename... Ts>
      requires ((std::constructible_from<Argument, Ts> && ...) &&
                !(sizeof...(Ts) == 1 && std::same_as<std::decay_t<Ts>..., Format>))
    Format(Ts&&... ts)
      : args{ Argument(std::forward<Ts>(ts))... }
    {}
};
```
Здесь:
- ((std::constructible_from<Argument, Ts> && ...)) — проверяет, что каждый Ts может быть использован для конструктора Argument.
- выражение с sizeof...(Ts) && std::same_as предотвращает попадание шаблонного конструктора в ситуацию копирования Format (один аргумент типа Format).

C++11/14 (SFINAE)
```cpp
#include <type_traits>
#include <vector>
#include <utility>

struct Argument {
    template<typename T>
    explicit Argument(T&& v) { /* ... */ }
};

template<bool B, class T = void> using enable_if_t = typename std::enable_if<B,T>::type;

template<typename U, typename... Ts>
struct all_constructible;

template<typename U>
struct all_constructible<U> : std::true_type {};

template<typename U, typename T0, typename... Ts>
struct all_constructible<U, T0, Ts...>
  : std::integral_constant<bool,
        std::is_constructible<U, T0&&>::value && all_constructible<U, Ts...>::value> {};

struct Format {
    std::vector<Argument> args;

    Format() = default;
    Format(Format const&) = default;
    Format(Format&&) = default;

    template<typename... Ts, class = enable_if_t<
        all_constructible<Argument, Ts...>::value &&
        !(sizeof...(Ts) == 1 && std::is_same<typename std::decay<Ts>::type..., Format>::value)
    >>
    Format(Ts&&... ts)
      : args{ Argument(std::forward<Ts>(ts))... }
    {}
};
```
(в C++14 можно заменить all_constructible на std::conjunction и std::is_constructible прямо)

4) Дополнительные нюансы и советы
- is_constructible vs is_convertible: is_constructible учитывает explicit-конструкторы; is_convertible проверяет возможность неявного преобразования. Выбирайте то, что вам нужно.
- Помните про эффекты шаблонных конструкторов на генерацию move/copy: объявите =default, если хотите их сохранить.
- Делайте конструктор Argument explicit, если не хотите неявных преобразований.
- Для удобства и читаемости в C++20 используйте концепты/constraints.
- Если встречаются неоднозначности с std::initializer_list, добавьте отдельный overload или SFINAE для этого случая.

Если хотите — пришлите текущую реализацию классов Argument и Format, я покажу конкретные правки и готовый код с ограничениями, подходящими для вашего случая.
Кажется, понял. <br/> <br/> В случае со строками как раз строгость проверки надо убавить: <br/> <br/> <pre><code class="cpp">template &lt;typename T&gt;
concept WideCompatible =
	std::convertible_to&lt;T, const wchar_t*&gt; ||
	std::same_as&lt;T, std::wstring&gt;;

template &lt;WideCompatible T&gt;
void Argument(T const&amp; wideString)
{
	m_strValue = wideString;
}</code></pre>
Похожие вопросы