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::duration
(std::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 内失败
那么如何解决呢?两种方式:
标记转换运算符const
。然而,这将为 m
和 i
选择转换为 int64_t
。
将转换为 int64_t
标记为 explicit
。现在模板化重载不会参与 m
.
的重载决议
最后,为什么这在 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 模式下都走这条路,这就是今天示例编译的原因。
以下代码使用设置为 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::duration
(std::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
.
那么如何解决呢?两种方式:
标记转换运算符
const
。然而,这将为m
和i
选择转换为int64_t
。将转换为
int64_t
标记为explicit
。现在模板化重载不会参与m
. 的重载决议
最后,为什么这在 C++17 中有效?那将保证复制省略。由于你的 Convert
有一个到 std::chrono::milliseconds
的转换,它被用来直接初始化 m
。它的细节包括甚至不需要选择复制构造函数,只是稍后将其删除。
这是一个非常有趣的案例。 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++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 模式下都走这条路,这就是今天示例编译的原因。