приоритет конструктора класса с вариативным конструктором шаблона для оболочки значения

Сегодня я обнаружил, что не понимаю правила приоритета конструктора 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), своего рода конструктором перемещения (T && v0) и своего рода шаблонной конструкцией на месте. -конструктор значений (As && ... as, по примеру emplace методов для контейнеров STL).

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

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

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

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 предпочтительнее, но не является конструктором std::string, который принимает значение 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?


person max66    schedule 20.08.2018    source источник
comment
Я не смотрел слишком внимательно, но я думаю, это потому, что вы передаете параметры типа T& и wrapper&, а не T const& и wrapper const&. Таким образом, идеальный конструктор переадресации находится первым. Возможные решения включают отключение идеального конструктора пересылки, если другие конструкторы совпадают (через SFINAE), или, возможно, добавление перегрузки T& и wrapper& (я не видел, чтобы это решение часто использовалось, но оно должно работать)   -  person Justin    schedule 20.08.2018
comment
Да, конструкторы шаблонов с переменным числом аргументов кусаются, и в очень деликатных частях тоже (!) — они выбираются, когда вы меньше всего этого ожидаете. На мой взгляд, лучший способ справиться с ними — использовать специальный тип тега, который будет выбирать его (намного проще, чем включать их с помощью трюков sfinae). Просто добавьте к ним префикс, например, in_place_t (украденный у std::variant) или любой другой тег по вашему выбору.   -  person SergeyA    schedule 20.08.2018
comment
Если вы измените подпись ctor шаблона variadiac на wrapper (const As && ... as), она больше не будет вызываться в пользу вашего ctor значения.   -  person cplusplusrat    schedule 20.08.2018
comment
@Justin - вы правы для T& (при передаче std::string const вызывается конструктор T const &), но (я не понимаю почему) для wrapper &.   -  person max66    schedule 20.08.2018
comment
@cplusplusrat Но тогда это не идеальный конструктор пересылки; он будет принимать только rvalue (из которых вы даже (безопасно) не можете перейти)   -  person Justin    schedule 20.08.2018
comment
@SergeyA - да: кусаются и очень болят. Пожалуйста, напишите in_place_t предложение как ответ; это не кажется мне действительно удовлетворительным, но это лучше, чем ничего.   -  person max66    schedule 20.08.2018
comment
@cplusplusrat - спасибо, но, как указал Джастин, const несовместим с идеальной пересылкой; во всяком случае, ваше наблюдение, кажется, подтверждает, что это проблема константности   -  person max66    schedule 20.08.2018
comment
@ max66 сделано по запросу.   -  person SergeyA    schedule 20.08.2018
comment
@Justin - Вы были совершенно правы также для wrapper & и wrapper const &: сделать w0 константой, w3 скомпилировать с конструктором копирования; это w4 выдает ошибку (надо еще поспать!).   -  person max66    schedule 20.08.2018


Ответы (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 и приемника являются взаимоисключающими, используйте только один из них.

person Barry    schedule 20.08.2018
comment
Прежде всего, спасибо за ваш ответ; вы правы насчет w0, мне нравится ваше решение std::is_constructible, и я согласен, что w0 можно поставить. Но я до сих пор не понимаю, почему emplace contructor предпочтительнее wrapper const & для w3, когда я делаю w0 const. - person max66; 20.08.2018
comment
@ max66 Это не так. - person Barry; 20.08.2018
comment
Я получаю ту же ошибку, когда пишу wrapper<std::string> const w0{s0}; wrapper<std::string> w3{w0};; как из g++ 6.3.0, так и clang++ 3.8.1 (хорошо: я использую старую платформу) - person max66; 20.08.2018
comment
@ max66 Оба этих компилятора правильно принимают конструкцию w3 в этой ситуации. Я не знаю, о чем ты говоришь? Вы уверены, что это w3, а не w4 в исходном примере, поскольку w0 теперь const? - person Barry; 20.08.2018
comment
Извините: я идиот: это w4: сейчас const и теперь я не могу двигаться. Еще раз спасибо: теперь все ясно - person max66; 20.08.2018
comment
Мне нравится избегать неявного emplace с struct emplace_tag в качестве первого аргумента, вызывающего его. Но это может не соответствовать проблемному пространству ОП. И теперь я вижу, что SergyA предложил это в другом ответе. - person Yakk - Adam Nevraumont; 21.08.2018
comment
::is_same<std::decay_t<A>, T>::value должно быть wrapper не T нет? Чтобы избежать wrapper&. Также какая польза от wrapper(T v) ctor, просто используйте emplace. - person Yakk - Adam Nevraumont; 21.08.2018
comment
@Yakk да, спасибо. И пара emplace/sink должна быть взаимоисключающей. Вроде либо огневые точки, либо просто топить. - person Barry; 21.08.2018
comment
@barry, но приемник избыточен и менее эффективен и теперь вызывает неоднозначность перегрузки. За исключением, может быть, ctor на основе ({}). - person Yakk - Adam Nevraumont; 21.08.2018
comment
@Yakk Избыточный и двусмысленный с чем? - person Barry; 21.08.2018
comment
@Barry Автор места? T уже можно использовать для создания T через emplace. Ах, это не будет двусмысленно, потому что нешаблон бьет шаблон, когда он связан. - person Yakk - Adam Nevraumont; 21.08.2018
comment
@Yakk-AdamNevraumont У вас не будет обоих. Взаимоисключающий. Либо ставить, либо тонуть. - person Barry; 21.08.2018

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

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

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

Я лично предпочитаю подход на основе тегов и использую тип тега в качестве первого аргумента для вариационного конструктора. В то время как любая структура тегов будет работать, я склонен (лениво) красть тип из С++ 17 - std::in_place. Теперь код становится:

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

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

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

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

person SergeyA    schedule 20.08.2018
comment
Спасибо. Вообще говоря, я предпочитаю решение Барри. Но я обнаружил проблему внутри класса, который я пишу, и для моей реальной проблемы я думаю, что приму ваше решение (не меняйте интерфейс библиотеки, которую я пишу). К сожалению, я пишу библиотеку C++11, поэтому не могу использовать std::in_place, но напишу внутреннюю структуру. - person max66; 21.08.2018

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

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

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

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

 };

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

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

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

person Oliv    schedule 20.08.2018
comment
Да... конструктор пересылки - еще один вариант; к сожалению, я пишу библиотеку C++11, поэтому не могу использовать std::as_const; но написать замену не сложно. Спасибо. - person max66; 21.08.2018