Оператор преобразования C++ в chrono::duration - работает с C++17, но не с C++14 или менее
Следующий код компилируется с gcc 7.1.0 с установленным C++17, но не компилируется с набором C++14 (или Visual Studio 2017). Это легко воспроизвести на Wandbox.
Что нужно сделать, чтобы он работал с C++11/14?
#include <iostream>
#include <chrono>
int main()
{
struct Convert
{
operator std::chrono::milliseconds()
{
std::cout << "operator std::chrono::milliseconds" << std::endl;
return std::chrono::milliseconds(10);
}
operator int64_t ()
{
std::cout << "operator int64_t" << std::endl;
return 5;
}
};
Convert convert;
std::chrono::milliseconds m(convert);
std::cout << m.count() << std::endl;
int64_t i(convert);
std::cout << i << std::endl;
}
2 ответа
Давайте начнем с того, почему это не работает в C++14. Есть два соответствующих c'tors для std::chrono::duration
(который std::chrono::milliseconds
алиас быть):
duration( const duration& ) = default;
template< class Rep2 >
constexpr explicit duration( const Rep2& r );
Шаблонный намного лучше подходит для аргумента типа Convert
, Но он будет участвовать только в разрешении перегрузки, если Rep2
(ака Convert
) неявно преобразуется в тип представления std::chrono::duration
, За milliseconds
, то есть long
на Wandbox. Ваш int64_t
Оператор преобразования делает возможным это неявное преобразование.
Но здесь есть подвох. Проверка этого неявного преобразования не учитывает cv-квалификаторы функции-члена преобразования. Так что эта перегрузка выбрана, но она принимает константную ссылку. И ваш пользовательский оператор преобразования не const
Квалифицированный! Galik отметил это в комментариях к вашему посту. Таким образом, преобразование не проходит внутри milliseconds
,
Так как это решить? Два пути:
Отметить оператор конвертации
const
, Это, однако, выберет преобразование вint64_t
для обоихm
а такжеi
,Отметить преобразование в
int64_t
какexplicit
, Теперь шаблонная перегрузка не будет участвовать в разрешении перегрузки дляm
,
И наконец, почему это работает в C++17? Это было бы гарантировано копия elision. Так как ваш Convert
имеет преобразование в std::chrono::milliseconds
используется для инициализации m
непосредственно. Минус этого включает в себя даже не необходимость выбирать конструктор копирования, просто чтобы исключить его позже.
Это довольно интересный случай. Причина, по которой он не компилируется на C++14, правильно объяснена StoryTeller и, возможно, является дефектом LWG - требование к конструктору преобразования заключается в том, что Rep2
конвертируется в rep
, но тело этого конструктора пытается преобразовать Rep2 const
в rep
- и в конкретном примере в OP это плохо сформировано. Это сейчас LWG 3050.
Однако в C++17 ни одно из соответствующих четко сформулированных правил в стандарте не изменилось. Прямая инициализация (например, std::chrono::milliseconds m(convert);
) по-прежнему рассматривает только конструкторы, и наилучшим соответствием по разрешению перегрузки среди конструкторов будет все тот же дефектный конвертирующий конструктор, который приводит к сбою программы в C++14.
Тем не менее, существует нерешенная основная проблема, которую gcc и clang, по-видимому, решили реализовать, несмотря на то, что пока нет формулировок для нее. Рассматривать:
struct A
{
A();
A(const A&) = delete;
};
struct B
{
operator A();
};
B b;
A a1 = b; // OK
A a2(b); // ?
Согласно языковым правилам сегодня, копировать-инициализировать из b
все в порядке, мы используем функцию преобразования. Но прямая инициализация из b
не в порядке, мы должны были бы использовать A
Конструктор удаленных копий.
Другой мотивирующий пример:
struct Cat {};
struct Dog { operator Cat(); };
Dog d;
Cat c(d);
Здесь, опять же, мы должны были бы пройти Cat(Cat&& )
- который в этом случае является правильно сформированным, но препятствует исключению копии из-за временной материализации.
Таким образом, предлагаемое решение этого вопроса состоит в том, чтобы рассмотреть как конструкторы, так и функции преобразования для прямой инициализации. В обоих примерах здесь и в примере OP это привело бы к "ожидаемому" поведению - прямая инициализация просто использовала бы функцию преобразования как лучшее соответствие. И gcc, и clang идут по этому пути в режиме C++17, поэтому примеры компилируются сегодня.