Как я могу определить, пересылаю ли я конструктор копирования?

#c #templates

#c #шаблоны

Вопрос:

Если я пишу универсальную функцию, которая пересылает аргументы в конструктор, есть ли способ определить, является ли это конструктором копирования? По сути, я хочу сделать:

 template <typename T, typename... Args>
void CreateTAndDoSomething(Argsamp;amp;... args) {
  // Special case: if this is copy construction, do something different.
  if constexpr (...) { ... }

  // Otherwise do something else.
  ...
}
  

Лучшее, что я придумал, — это проверка sizeof...(args) == 1 , а затем просмотр std::is_same_v<Args..., const Tamp;> || std::is_same_v<Args..., Tamp;> . Но я думаю, что это упускает крайние случаи, такие как входные данные с изменяемыми параметрами и вещи, которые неявно конвертируются T .

Честно говоря, я не совсем уверен, что этот вопрос четко определен, поэтому не стесняйтесь сказать мне, что это не так (и почему). Если это поможет, вы можете предположить, что единственными конструкторами с одним аргументом для T являются T(const Tamp;) и T(Tamp;amp;) .

Если я прав, что это не определено четко, потому что конструктор копирования не является вещью, то, возможно, это можно сделать более точным, сказав: «как я могу определить T(std::forward<Args>(args)...) , выбирает ли выражение перегрузку, которая принимает const Tamp; ?

Комментарии:

1. не можете ли вы предоставить отдельную перегрузку, когда есть только один Args , и это a T ?

2. Зачем вам это нужно сейчас, если это копия? Может быть, некоторые случаи почти-копирования, но не совсем также должны попадать в одно и то же ведро? IOW, это проблема XY?

3. Извините, что не исключил проблему XY. Основная предпосылка заключается в том, что я работаю над неправильным дизайном API: я использую API, который требует, чтобы вызывающий объект делал что-то другое при копировании объекта по сравнению со всеми другими видами использования API, что раздражающе нерегулярно. Я хотел бы обернуть его в свой собственный слой, который упорядочивает это, обнаруживая вызов соответствующей перегрузки и выполняя специальные действия от имени пользователя. Тот факт, что это конструктор копирования, на самом деле не имеет значения; Мне просто нужно знать, какая перегрузка будет выбрана.

4. Вопрос не так четко определен, потому что конструктор копирования не обязательно принимает один аргумент. У вас все еще есть конструктор копирования, если первый аргумент является ссылкой const lvalue, а остальные аргументы по умолчанию. Обратитесь к class.copy.ctor . Таким образом, проверка sizeof...(args) == 1 технически некорректна в общем смысле.

5. То есть конструкция копирования вызывает конструктор, который квалифицируется как конструктор копирования. Но вызов конструктора, который квалифицируется как конструктор копирования, не обязательно является копированием, и если количество предоставленных аргументов != 1, то это не копирование. Пересылка, где sizeof...(args) > 1 никогда не «перенаправляется в конструктор копирования», даже если она перенаправляется в функцию конструктора, которая также является конструктором копирования, она не действует как конструктор копирования в этом вызове.

Ответ №1:

Вы можете использовать remove_cv_t:

 #include <type_traits>

template <typename T, typename... Args>
void CreateTAndDoSomething(Argsamp;amp;... args) {
  // Special case: if this is copy construction, do something different.
  if constexpr (sizeof...(Args) == 1 amp;amp; is_same_v<Tamp;, remove_cv_t<Args...> >) { ... }

  // Otherwise do something else.
  ...
}
  

Это охватывает все «конструкторы копирования», определенные стандартом, не учитывая возможные аргументы по умолчанию (трудно определить, является ли данный параметр функции — для функции, которая будет вызвана с учетом этих параметров — по умолчанию или нет).

Комментарии:

1. параметры по умолчанию не являются проблемой, они не пересылаются через CreateTAndDoSomething слой и не будут учитываться при sizeof...(Args) их вставке компилятором в момент вызова T(std::forward<Args>(args)...)

2. @BenVoigt Конечно, но если у вас есть конструктор A(const Aamp;, int = 0) , это конструктор копирования в соответствии со стандартом, даже если вы вызываете его как new A(other_a, 123) . В этом случае он должен считаться конструктором копирования, если Args есть const Aamp;, int , но в моем коде в настоящее время это не так.

3. Спасибо! Я вижу, что аргументы по умолчанию не охвачены этим решением, но в моем конкретном случае мне не нужно беспокоиться о них (я могу предположить, что их нет T ).

4. На самом деле мне приходит в голову, что этот ответ не распространяется на неявные преобразования. Можно ли определить, приведет ли выбранная перегрузка к неявному преобразованию аргумента в тип назначения перед его вызовом?

5. Ну, тогда это не будет конструктор копирования, но тогда вместо is_same_v вы можете использовать is_convertible_v<Аргументы …, T> .

Ответ №2:

У вас была правильная идея. Все, что необходимо, закодировано в выведенном типе Args . Хотя, если вы хотите учесть все случаи, указанные в резюме, вам придется многое пройти. Давайте сначала рассмотрим различные случаи, которые могут возникнуть:

  1. Построение (неявные преобразования — это построение)
  2. Построение копирования (обычно T(const Tamp;) )
  3. Перемещение конструкции (обычно T(Tamp;amp;) )
  4. Нарезка (вызов Base(const Baseamp;) или Base(Baseamp;amp;) с Derived помощью)

Если странные конструкторы перемещения или копирования не рассматриваются (с параметрами по умолчанию), случаи 2-4 могут произойти только при передаче одного аргумента, все остальное является конструкцией. Следовательно, разумно обеспечить перегрузку для случая с одним аргументом. Попытка выполнить все эти случаи в шаблоне variadic будет некрасивой, так как вам нужно использовать выражения fold или что-то в этом роде std::conjuction/std::disjuction , чтобы if операторы были действительными.

Мы также узнаем, что распознавание перемещения и копирования по отдельности в каждом отдельном случае невозможно. Если нет необходимости рассматривать копии и перемещения отдельно, решение простое. Но если эти случаи необходимо разделить, можно только сделать хорошее предположение, которое должно работать почти всегда.

Что касается нарезки, я бы, вероятно, предпочел отключить ее с помощью a static_assert .

Комбинированное перемещение и копирование

Вот решение, использующее перегрузку одного аргумента. Давайте рассмотрим это подробно далее.

 #include <utility>
#include <type_trait>
#include <iostream>


// Multi-argument case is almost always construction
template<typename T, typename... Args>
void CreateTAndDoSomething(Argsamp;amp;... args)
{   
    std::cout << "Constructed" << 'n';
    T val(std::forward<Args>(args)...);
}

template<typename T, typename U>
void CreateTAndDoSomething(Uamp;amp; arg)
{
    // U without references and cv-qualifiers
    // std::remove_cvref_t in C  20
    using StrippedU = std::remove_cv_t<std::remove_reference_t<U>>;

    // Extra check is needed because T is a base for itself
    static_assert(
        std::is_same_v<StrippedU, T> || !std::is_base_of_v<T, StrippedU>, 
        "Attempting to slice"
    );
    
    if constexpr (std::is_same_v<StrippedU, T>)
    {
        std::cout << "Copied or moved" << 'n';
    }
    else
    {
        std::cout << "Constructed" << 'n';
    }
    
    T val(std::forward<U>(arg));
}
  

Здесь мы используем тот факт, что Uamp;amp; Argsamp;amp; ) является ссылкой пересылки. При пересылке ссылок выводимый аргумент шаблона U отличается в зависимости от категории передаваемого значения arg . Учитывая arg тип T , U выводится так, что:

  • Если arg было значение lvalue, выводится U Tamp; значение (включая cv-квалификаторы).
  • Если arg это было значение rvalue, выводится U T значение (включая cv-квалификаторы).

ПРИМЕЧАНИЕ: U может привести к ссылке, соответствующей cv (например. ). удаляет только const Fooamp; std::remove_cv cv-квалификаторы верхнего уровня, а ссылки не могут иметь cv-квалификаторы верхнего уровня. Вот почему std::remove_cv необходимо применять к не ссылочному типу. Если бы std::remove_cv использовался только, шаблон не смог бы распознать случаи, когда U было бы const Tamp; , volatile Tamp; или const volatile Tamp; .

Только копировать

Конструктор копирования вызывается (обычно, см. Примечание), когда U выводится в Tamp; const Tamp; , volatile Tamp; или const volatile Tamp; . Поскольку у нас есть три случая, когда выведенный U является ссылкой, соответствующей cv, и std::remove_cv не работает с ними, мы должны просто явно проверить эти случаи:

 template<typename T, typename U>
void CreateTAndDoSomething(Uamp;amp; arg)
{
    // U without references and cv-qualifiers
    // std::remove_cvref_t in C  20
    using StrippedU = std::remove_cv_t<std::remove_reference_t<U>>;

    // Extra check is needed because T is a base for itself
    static_assert(
        std::is_same_v<StrippedU, T> || !std::is_base_of_v<T, StrippedU>, 
        "Attempting to slice"
    );
    
    if constexpr (std::is_same_v<Tamp;, U> 
        || std::is_same_v<const Tamp;, U>
        || std::is_same_v<volatile Tamp;, U>
        || std::is_same_v<const volatile Tamp;, U>)
    {
        std::cout << "Copied" << 'n';
    }
    else
    {
        std::cout << "Constructed" << 'n';
    }
    
    T val(std::forward<U>(arg));
}
  

ПРИМЕЧАНИЕ: это не распознает конструкцию копирования, когда конструктор перемещения недоступен, а конструктор копирования с подписью T(const Tamp;) доступен. Это потому, что результатом вызова std::forward с rvalue arg является xvalue , с которым можно связываться const Tamp; .

Перемещение и копирование разделены

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: это решение работает только для общего случая (см. Подводные камни)

Давайте предположим, что T у него есть конструктор копирования с подписью T(const Tamp;) и конструктор перемещения с подписью T(Tamp;amp;) , что действительно распространено. const -квалифицированные конструкторы перемещения на самом деле не имеют смысла, поскольку перемещаемый объект почти всегда нуждается в изменении.

С учетом этого предположения выражение T val(std::forward<U>(arg)); move создает val , если U было выведено на неконстантное T значение ( arg является неконстантным значением rvalue). Это дает нам два случая:

  1. U выводится в T
  2. U выводится в volatile T

Сначала удалив квалификатор volatile из U , мы можем учесть оба этих случая. Когда конструкция перемещения распознается первой, остальные — это конструкция копирования:

 template<typename T, typename U>
void CreateTAndDoSomething(Uamp;amp; arg)
{
    // U without references and cv-qualifiers
    using StrippedU = std::remove_cv_t<std::remove_reference_t<U>>;
    
    // Extra check is needed because T is a base for itself
    static_assert(
        std::is_same_v<StrippedU, T> || !std::is_base_of_v<T, StrippedU>, 
        "Attempting to slice"
    );
    
    if constexpr (std::is_same_v<std::remove_volatile_t<U>, T>)
    {
        std::cout << "Moved (usually)" << 'n';
    }
    else if constexpr (std::is_same_v<StrippedU, T>)
    {
        std::cout << "Copied (usually)" << 'n';
    }
    else
    {
        std::cout << "Constructed" << 'n';
    }
    
    T val(std::forward<U>(arg));
}
  

Если вы хотите поиграть с решением, оно доступно в godbolt. Я также реализовал специальный класс, который, надеюсь, поможет визуализировать различные вызовы конструктора.

Подводные камни решения

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

  1. Если конструктор перемещения для T недоступен, arg это значение типа rvalue T , а конструктор копирования имеет подпись T(const Tamp;) :

    Значение xvalue, возвращаемое с помощью std::forward<U>(arg) , будет привязано к const Tamp; . Это также обсуждалось в случае «только копия».

    Перемещение распознано, но происходит копирование.

  2. If T имеет конструктор перемещения с подписью T(const Tamp;amp;) и arg является постоянным значением типа T :

    Копия распознана, но происходит перемещение. Аналогичный случай с T(const volatile Tamp;amp;).

Я также решил не учитывать случай, когда пользователь явно указывает U ( Tamp;amp; и volatile Tamp;amp; будет компилироваться, но не распознается должным образом).