Оператор преобразования 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,

Так как это решить? Два пути:

  1. Отметить оператор конвертации const, Это, однако, выберет преобразование в int64_t для обоих m а также i,

  2. Отметить преобразование в 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, поэтому примеры компилируются сегодня.

Другие вопросы по тегам