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

2 ответа:

Давайте начнем с того, почему это не работает в C++14. Есть два соответствующих к'Тора для std::chrono::duration (который std::chrono::milliseconds имеет псевдоним):

duration( const duration& ) = default;

template< class Rep2 >
constexpr explicit duration( const Rep2& r );

Шаблонный гораздо лучше подходит для аргумента типа Convert. Но он будет участвовать в разрешении перегрузки только в том случае, если Rep2 (a.k.A Convert) неявно преобразуется в тип представления std::chrono::duration. Для milliseconds, то есть long на Wandbox. Ваш оператор преобразования int64_t делает это неявное преобразование возможным.

Но вот в чем загвоздка. Проверка этого неявного преобразования не учитывает CV-квалификаторы функции-члена преобразования. Так что перегрузка выбрана, но она принимает по ссылке const. И ваш определяемый пользователем оператор преобразования не является const квалифицированным! @Galik отметил это в комментариях к вашему посту. Как такое преобразование не внутри конструктор из milliseconds.

Так как же ее разрешить? Два способа:
  1. Отметьте оператор преобразования const. Тот однако мы выберем преобразование в int64_t как для m, так и для i.

  2. Отметьте преобразование в int64_t как explicit. Теперь шаблонная перегрузка не будет участвовать в разрешении перегрузки для m.

И, наконец, почему это работает в C++17? Это была бы гарантированная копия Элизии. Поскольку ваш 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, поэтому примеры компилируются сегодня.