C++ 转换运算符到 chrono::duration - 适用于 c++17 但不适用于 C++14 或更低版本

C++ conversion operator to chrono::duration - works with c++17 but not C++14 or less

以下代码使用设置为 C++17 的 gcc 7.1.0 进行编译,但不使用设置为 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;
}

让我们从为什么这在 C++14 中不起作用开始。 std::chrono::durationstd::chrono::milliseconds 的别名是)有两个相关的 c'tors:

duration( const duration& ) = default;

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

模板化的参数更适合 Convert 类型的参数。但只有当Rep2 (a.k.a Convert) 可以隐式转换为std::chrono::duration 的表示类型时,它才会参与重载决策。对于 milliseconds,即 Wandbox 上的 long。您的 int64_t 转换运算符使隐式转换成为可能。

但这就是问题所在。对此隐式转换的检查不考虑转换成员函数的 cv 限定符。因此选择了重载,但它通过 const 引用接受。并且您的用户定义的转换运算符不符合 const 资格! @Galik 在您的 post 的评论中指出了这一点。因此,转换在 milliseconds.

的 c'tor 内失败

那么如何解决呢?两种方式:

  1. 标记转换运算符const。然而,这将为 mi 选择转换为 int64_t

  2. 将转换为 int64_t 标记为 explicit。现在模板化重载不会参与 m.

  3. 的重载决议

最后,为什么这在 C++17 中有效?那将保证复制省略。由于你的 Convert 有一个到 std::chrono::milliseconds 的转换,它被用来直接初始化 m。它的细节包括甚至不需要选择复制构造函数,只是稍后将其删除。

这是一个非常有趣的案例。 , and is arguably an LWG defect - the requirement on the converting constructor is that Rep2 is convertible to rep, but the body of that constructor attempts to convert a Rep2 const to rep - and in the particular example in OP, this is ill-formed. This is now LWG 3050.

正确解释了它无法在 C++14 上编译的原因

虽然在 C++17 中,none 标准中的相关、明确的规则已经更改。直接初始化(例如 std::chrono::milliseconds m(convert);)仍然只考虑 constructors,构造函数中重载决议的最佳匹配仍然是导致程序无法在 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&& ) - 在这种情况下它是格式正确的,但由于 temporary materialization.

而禁止复制省略

所以这个问题的建议解决方案是考虑 构造函数 直接初始化的转换函数。在此处和 OP 示例中的两个示例中,这将产生 "expected" 行为 - 直接初始化将仅使用转换函数作为更好的匹配。 gcc 和 clang 在 C++17 模式下都走这条路,这就是今天示例编译的原因。