Сегодня я обнаружил, что не понимаю правила приоритета конструктора C ++.

Пожалуйста, смотрите следующий шаблон struct wrapper

template <typename T>
struct wrapper
 {
   T value;

   wrapper (T const & v0) : value{v0}
    { std::cout << "value copy constructor" << std::endl; }

   wrapper (T && v0) : value{std::move(v0)}
    { std::cout << "value move constructor" << std::endl; }

   template <typename ... As>
   wrapper (As && ... as) : value(std::forward<As>(as)...)
    { std::cout << "emplace constructor" << std::endl; }

   wrapper (wrapper const & w0) : value{w0.value}
    { std::cout << "copy constructor" << std::endl; }

   wrapper (wrapper && w0) : value{std::move(w0.value)}
    { std::cout << "move constructor" << std::endl; }
 };

Это простая оболочка значения шаблона с конструктором копирования (wrapper const &), конструктором перемещения (wrapper && w0), конструктором копирования значений (T const & v0), конструктором перемещения ({{ X3}}) и своего рода конструктор шаблон-конструктор-место-значение (As && ... as, следуя примеру методов emplace для контейнеров STL).

Мое намерение состояло в том, чтобы использовать конструктор копирования или перемещения, вызываемый с помощью оболочки, конструктор копирования или перемещения значения, передающий объект T, и конструктор emplace шаблона, вызывающий со списком значений, способных создать объект типа {{X1 } } .

Но я не понимаю, чего я ожидал.

Из следующего кода

std::string s0 {"a"};

wrapper<std::string> w0{s0};            // emplace constructor (?)
wrapper<std::string> w1{std::move(s0)}; // value move constructor
wrapper<std::string> w2{1u, 'b'};       // emplace constructor
//wrapper<std::string> w3{w0};          // compilation error (?)
wrapper<std::string> w4{std::move(w0)}; // move constructor

Значения w1, w2 и w4 создаются с помощью конструктора перемещения значений, конструктора emplace и конструктора перемещения (соответственно), как и ожидалось.

Но w0 создается с помощью конструктора emplace (я ожидал конструктора копирования значений), а w3 вообще не создается (ошибка компиляции), потому что конструктор emplace предпочтительнее, но не является {{X2} } конструктор, который принимает значение wrapper<std::string>.

Первый вопрос: что я делаю не так?

Я предполагаю, что проблема w0 в том, что s0 не является значением const, поэтому T const & не является точным соответствием.

Действительно, если я напишу

std::string const s1 {"a"};

wrapper<std::string> w0{s1};  

Я получаю конструктор копирования значения под названием

Второй вопрос: что мне нужно сделать, чтобы получить то, что я хочу?

Так что мне нужно сделать, чтобы конструктор копирования значений (T const &) получил приоритет над конструктором emplace (As && ...) также с неконстантными значениями T и, в основном, с тем, что я нужно сделать, чтобы конструктор копирования (wrapper const &) взял на себя приоритет конструирования w3?

10
max66 20 Авг 2018 в 22:41

3 ответа

Лучший ответ

Нет такой вещи как «правила приоритета конструктора», нет ничего особенного в конструкторах с точки зрения приоритета.

Два проблемных случая имеют одно и то же основное правило, объясняющее их:

wrapper<std::string> w0{s0};            // emplace constructor (?)
wrapper<std::string> w3{w0};            // compilation error (?)

Для w0 у нас есть два кандидата: конструктор копирования значения (который принимает std::string const&) и конструктор emplace (который принимает std::string&). Последнее лучше подходит, потому что его ссылка менее квалифицирована по cv, чем ссылка конструктора копирования значения (в частности, [over.ics.rank] / 3). Укороченная версия этого:

template <typename T> void foo(T&&); // #1
void foo(int const&);                // #2

int i;
foo(i); // calls #1

Аналогично, для w3 у нас есть два кандидата: конструктор emplace (который принимает wrapper&) и конструктор копирования (который принимает wrapper const&). Опять же, из-за того же правила, конструктор emplace является предпочтительным. Это приводит к ошибке компиляции, потому что value на самом деле не конструируем из wrapper<std::string>.

Вот почему вы должны быть осторожны с пересылкой ссылок и ограничивать свои шаблоны функций! Это пункт 26 («Избегать перегрузки универсальных ссылок») и пункт 27 («Ознакомьтесь с альтернативами перегрузки универсальных ссылок») в Effective Modern C ++. Голый минимум будет:

template <typename... As,
    std::enable_if_t<std::is_constructible<T, As...>::value, int> = 0>
wrapper(As&&...);

Это позволяет w3, потому что теперь есть только один кандидат. Тот факт, что w0 использует вместо копий не имеет значения, конечный результат тот же. На самом деле, конструктор копирования значений в любом случае ничего не делает - вы должны просто удалить его.


Я бы порекомендовал этот набор конструкторов:

wrapper() = default;
wrapper(wrapper const&) = default;
wrapper(wrapper&&) = default;

// if you really want emplace, this way
template <typename A=T, typename... Args,
    std::enable_if_t<
        std::is_constructible<T, A, As...>::value &&
        !std::is_same<std::decay_t<A>, wrapper>::value
        , int> = 0>
wrapper(A&& a0, Args&&... args)
  : value(std::forward<A>(a0), std::forward<Args>(args)...)
{ }

// otherwise, just take the sink
wrapper(T v)
  : value(std::move(v))
{ }

Это делается с минимальной суетой и путаницей. Обратите внимание, что конструкторы emplace и sink являются взаимоисключающими, используйте только один из них.

8
Barry 21 Авг 2018 в 13:42

Как сказано в комментарии, проблема заключается в том, что конструктор шаблона переменной принимает аргумент путем пересылки ссылки, поэтому он лучше подходит для неконстантных копий или константных копий.

Есть много способов отключить его, один эффективный способ - всегда использовать тег как in_place_t, как предложено SergeyA в его ответе. Другой - отключить конструктор шаблона, когда он совпадает с сигнатурой конструктора копирования, как это предлагается в известных книгах по Effective C ++.

В этом случае я предпочитаю объявлять все возможные сигнатуры для конструкторов копирования / перемещения (а также назначения копирования / перемещения). Таким образом, независимо от того, какой новый конструктор я добавлю в класс, мне не придется думать о том, как избежать создания копии, это короткая двухстрочная часть кода, легко читаемая и не загрязняющая интерфейс других конструкторов:

template <typename T>
struct wrapper
 {
   //...
   wrapper (wrapper& w0) : wrapper(as_const(w0)){}
   wrapper (const wrapper && w0) : wrapper(w0){}

 };

Примечание: это решение не следует использовать, если планируется использовать его в качестве энергозависимого типа или если выполнены все следующие условия:

  • размер объекта меньше 16 байт (или 8 байт для MSVC ABI),
  • все члены субъекта тривиально копируемые,
  • эта оболочка будет передана функциям, где особое внимание уделяется случаю, когда аргумент имеет тип тривиально копируемого типа, а его размер меньше, чем предыдущий порог, поскольку в этом случае аргумент может быть передан в регистр 2) передавая аргумент по значению!

Если все эти требования выполнены, то вы можете подумать о реализации менее обслуживаемого (подверженного ошибкам -> в следующий раз, когда кто-нибудь изменит код) или решения, загрязняющего интерфейс клиента!

1
Oliv 20 Авг 2018 в 21:15

Как предложила ОП, выкладываю мой комментарий в качестве ответа с некоторыми уточнениями.

Из-за того, как выполняется разрешение перегрузки и типы сопоставляются, в качестве наилучшего соответствия часто выбирается тип конструктора прямой ссылки с переменными ссылками. Это может произойти, потому что все квалификации const будут разрешены правильно и сформируют идеальное соответствие - например, при привязке ссылки const к неконстантному lvalue и тому подобному - как в вашем примере.

Один из способов справиться с ними - отключить (с помощью различных методов sfinae, имеющихся в нашем распоряжении) конструктор переменных, когда список аргументов совпадает (хотя и несовершенно) с любым другим доступным конструктором, но это очень утомительно и требует постоянной поддержки, когда требуются дополнительные конструкторы. добавлено .

Лично я предпочитаю подход, основанный на тегах, и использую тип тега в качестве первого аргумента для конструктора переменных. Хотя любая структура тегов будет работать, я склонен (лениво) красть тип из C ++ 17 - std::in_place. Код теперь становится:

template<class... ARGS>
Constructor(std::in_place_t, ARGS&&... args)

А чем называется

Constructor ctr(std::in_place, /* arguments */);

Поскольку в моем опыте в месте вызова природа конструктора всегда известна - т.е. вы всегда будете знать, намереваетесь ли вы вызвать конструктор, принимающий прямые ссылки, или нет - это решение хорошо работает для меня.

4
SergeyA 20 Авг 2018 в 20:25
51937519