编写一个只接受文字“0”或文字“1”作为参数的函数
Write a function that only accepts literal `0` or literal `1` as argument
有时对于代数类型,使用一个构造函数会很方便,该构造函数采用文字值 0
来表示中性元素,或 1
来表示乘法单位元素,即使基础类型不是整数。
问题是如何说服编译器只接受 0
或 1
而不接受任何 other 整数并不明显。
有没有办法在 C++14 或更高版本中执行此操作,例如组合文字、constexpr 或 static_assert?
让我用一个自由函数来说明(尽管我的想法是将这种技术用于采用单个参数的构造函数。构造函数也不能采用模板参数)。
一个只接受零的函数可以这样写:
constexpr void f_zero(int zero){assert(zero==0); ...}
问题是,这只能在运行时失败。我可以写 f_zero(2)
甚至 f_zero(2.2)
程序仍然可以编译。
第二种情况很容易去除,例如使用enable_if
template<class Int, typename = std::enable_if_t<std::is_same<Int, int>{}> >
constexpr void g_zero(Int zero){assert(zero==0);}
这仍然存在我可以传递任何整数的问题(而且它只在调试模式下失败)。
在 C++ pre 11 中,可以使用此技巧来只接受文字零。
struct zero_tag_{};
using zero_t = zero_tag_***;
constexpr void h_zero(zero_t zero){assert(zero==nullptr);}
这实际上允许 99% 的人在那里,除了非常难看的错误消息。
因为,基本上(模 Maquevelian 使用),唯一接受的参数是 h_zero(0)
.
这里是事态说明https://godbolt.org/z/wSD9ri。
我看到 Boost.Units 库中使用了这种技术。
1) 现在可以使用 C++ 的新功能做得更好吗?
我问的原因是因为使用文字 1
上述技术完全失败。
2) 是否有可以应用于文字 1
案例的等效技巧?(最好作为一个单独的函数)。
我可以想象可以发明一个非标准的 long long 文字 _c
来创建 std::integral_constant<int, 0>
或 std::integral_constant<int, 1>
的实例,然后使函数采用这些类型。但是,对于 0
情况,生成的语法将是最糟糕的。也许有更简单的东西。
f(0_c);
f(1_c);
编辑:我应该提到,由于 f(0)
和 f(1)
可能是完全独立的函数,因此理想情况下它们应该调用不同的函数(或重载) .
您可以通过将 0 或 1 作为模板参数传递来获得此信息,如下所示:
template <int value, typename = std::enable_if_t<value == 0 | value == 1>>
void f() {
// Do something with value
}
函数将被调用为:f<0>()
。我不相信可以为构造函数做同样的事情(因为你不能为构造函数显式设置模板参数),但你可以将构造函数设为私有并具有静态包装函数,可以给模板参数执行检查:
class A {
private:
A(int value) { ... }
public:
template <int value, typename = std::enable_if_t<value == 0 || value == 1>>
static A make_A() {
return A(value);
}
};
A
类型的对象将使用 A::make_A<0>()
创建。
这不是一个现代解决方案,但添加到 Zach Peltzer 的解决方案中,如果您使用宏,您可以保留您的语法...
template <int value, typename = std::enable_if_t<value == 0 | value == 1>>
constexpr int f_impl() {
// Do something with value
return 1;
}
#define f(x) f_impl<x>()
int main() {
f(0); //ok
f(1); //ok
f(2); //compile time error
}
不过,对于构造函数问题,您可以将 class 模板化,而不是尝试使用模板化构造函数
template<int value, typename = std::enable_if_t<value == 0 | value == 1>>
class A {
public:
A() {
//do stuff
}
};
int main() {
A<0> a0;
auto a1 = A<1>();
// auto a2 = A<2>(); //fails!
}
嗯...你已经标记了 C++17,所以你可以使用 if constexpr
。
所以当0_x
是一个std::integral_constant<int, 0>
值,当1_x
是一个std::integral_constant<int, 1>
,当2_x
(以及其他值)给出编译错误。
举例
template <char ... Chs>
auto operator "" _x()
{
using t0 = std::integer_sequence<char, '0'>;
using t1 = std::integer_sequence<char, '1'>;
using tx = std::integer_sequence<char, Chs...>;
if constexpr ( std::is_same_v<t0, tx> )
return std::integral_constant<int, 0>{};
else if constexpr ( std::is_same_v<t1, tx> )
return std::integral_constant<int, 1>{};
}
int main ()
{
auto x0 = 0_x;
auto x1 = 1_x;
//auto x2 = 2_x; // compilation error
static_assert( std::is_same_v<decltype(x0),
std::integral_constant<int, 0>> );
static_assert( std::is_same_v<decltype(x1),
std::integral_constant<int, 1>> );
}
现在你的f()
函数可以
template <int X, std::enable_if_t<(X == 0) || (X == 1), bool> = true>
void f (std::integral_constant<int, X> const &)
{
// do something with X
}
你可以这样调用它
f(0_x);
f(1_x);
在 C++20 中,您可以使用 consteval
关键字强制编译时求值。有了它,您可以创建一个结构,它有一个 consteval
构造函数并将其用作函数的参数。像这样:
struct S
{
private:
int x;
public:
S() = delete;
consteval S(int _x)
: x(_x)
{
if (x != 0 && x != 1)
{
// this will trigger a compile error,
// because the allocation is never deleted
// static_assert(_x == 0 || _x == 1); didn't work...
new int{0};
}
}
int get_x() const noexcept
{
return x;
}
};
void func(S s)
{
// use s.get_x() to decide control flow
}
int main()
{
func(0); // this works
func(1); // this also works
func(2); // this is a compile error
}
这里还有一个godbolt example。
编辑:
显然 clang 10
不会像 here 那样给出错误,但是 clang (trunk)
在 godbolt 上会出现错误。
有一个基本问题。您如何才能在编译器中为一个参数完成这些工作,同时又高效呢?那么,您到底需要什么?
这包含在 Pascal 或 Ada 等强类型语言中。枚举类型只有几个值,通常在开发时检查类型,但除此之外,检查会在 运行 时被某些编译器选项消除,因为 一切顺利.
一个函数接口就是一个契约。它是卖方(功能的编写者)和买方(该功能的用户)之间的合同。甚至还有一种仲裁程序,即编程语言,如果有人试图欺骗合约,它可以采取行动。但最后,该程序 运行 在一台开放的机器中进行任意操作,例如修改枚举值集并放置一个完整的(且不允许的值)。
单独编译也有问题。单独编译有它的缺点,因为它必须面对一个编译,而不必重新检查和重新测试你以前所做的所有编译。编译完成后,您放入代码中的所有内容都在那里。如果你希望代码高效,那么测试是多余的,因为调用者和实现者都处理契约,但如果你想抓住一个谎言,那么你必须包含测试代码。然后,对所有情况都做一次更好,还是让程序员决定什么时候和什么时候不想抓 lyer 更好?
C 的问题(以及 C++ 的遗留问题)是他们受到非常优秀的程序员的启发,他们没有错误,并且必须 运行 他们的软件在大而慢的机器上。他们决定使这两种语言(第二种语言是为了互操作性目的)成为弱类型的……它们就是这样。你试过用 Ada 编程吗?还是 Modula-2?你会发现,随着时间的推移,强类型的东西比其他东西更学术,最后,作为专业人士,你想要的是自由地说:现在我想要安全(并包括测试代码) ,现在我知道我在做什么了(请尽可能高效)
结论
结论是你可以自由select语言,select编译器,放宽规则。编译器有可能允许你这样做。你必须应对它,或者发明(这是今天几乎每周都会发生的事情)你自己的编程语言。
对于 Ada,您可以定义子类型、新类型或仅受整数 0 和 1 值约束的派生类型。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure two_value is
-- You can use any one of the following 3 declarations. Just comment out other two.
--subtype zero_or_one is Integer range 0 .. 1; -- subtype of Integer.
--type zero_or_one is range 0 .. 1; -- new type.
type zero_or_one is new Integer range 0 .. 1; -- derived type from Integer.
function get_val (val_1 : in zero_or_one) return Integer;
function get_val (val_1 : in zero_or_one) return Integer is
begin
if (val_1 = 0) then
return 0;
else
return 1;
end if;
end get_val;
begin
Put_Line("Demonstrate the use of only two values");
Put_Line(Integer'Image(get_val(0)));
Put_Line(Integer'Image(get_val(1)));
Put_Line(Integer'Image(get_val(2)));
end two_value;
编译时您会收到以下警告消息,尽管编译成功:
>gnatmake two_value.adb
gcc -c two_value.adb
two_value.adb:29:40: warning: value not in range of type "zero_or_one" defined at line 8
two_value.adb:29:40: warning: "Constraint_Error" will be raised at run time
gnatbind -x two_value.ali
gnatlink two_value.ali
执行它会出现编译器指定的运行时错误
>two_value.exe
Demonstrate the use of only two values
0
1
raised CONSTRAINT_ERROR : two_value.adb:29 range check failed
所以,基本上你可以通过定义新类型、派生类型或子类型来约束值,你不需要包含检查范围的代码,但编译器会根据你的数据类型自动警告你.
有时对于代数类型,使用一个构造函数会很方便,该构造函数采用文字值 0
来表示中性元素,或 1
来表示乘法单位元素,即使基础类型不是整数。
问题是如何说服编译器只接受 0
或 1
而不接受任何 other 整数并不明显。
有没有办法在 C++14 或更高版本中执行此操作,例如组合文字、constexpr 或 static_assert?
让我用一个自由函数来说明(尽管我的想法是将这种技术用于采用单个参数的构造函数。构造函数也不能采用模板参数)。
一个只接受零的函数可以这样写:
constexpr void f_zero(int zero){assert(zero==0); ...}
问题是,这只能在运行时失败。我可以写 f_zero(2)
甚至 f_zero(2.2)
程序仍然可以编译。
第二种情况很容易去除,例如使用enable_if
template<class Int, typename = std::enable_if_t<std::is_same<Int, int>{}> >
constexpr void g_zero(Int zero){assert(zero==0);}
这仍然存在我可以传递任何整数的问题(而且它只在调试模式下失败)。
在 C++ pre 11 中,可以使用此技巧来只接受文字零。
struct zero_tag_{};
using zero_t = zero_tag_***;
constexpr void h_zero(zero_t zero){assert(zero==nullptr);}
这实际上允许 99% 的人在那里,除了非常难看的错误消息。
因为,基本上(模 Maquevelian 使用),唯一接受的参数是 h_zero(0)
.
这里是事态说明https://godbolt.org/z/wSD9ri。 我看到 Boost.Units 库中使用了这种技术。
1) 现在可以使用 C++ 的新功能做得更好吗?
我问的原因是因为使用文字 1
上述技术完全失败。
2) 是否有可以应用于文字 1
案例的等效技巧?(最好作为一个单独的函数)。
我可以想象可以发明一个非标准的 long long 文字 _c
来创建 std::integral_constant<int, 0>
或 std::integral_constant<int, 1>
的实例,然后使函数采用这些类型。但是,对于 0
情况,生成的语法将是最糟糕的。也许有更简单的东西。
f(0_c);
f(1_c);
编辑:我应该提到,由于 f(0)
和 f(1)
可能是完全独立的函数,因此理想情况下它们应该调用不同的函数(或重载) .
您可以通过将 0 或 1 作为模板参数传递来获得此信息,如下所示:
template <int value, typename = std::enable_if_t<value == 0 | value == 1>>
void f() {
// Do something with value
}
函数将被调用为:f<0>()
。我不相信可以为构造函数做同样的事情(因为你不能为构造函数显式设置模板参数),但你可以将构造函数设为私有并具有静态包装函数,可以给模板参数执行检查:
class A {
private:
A(int value) { ... }
public:
template <int value, typename = std::enable_if_t<value == 0 || value == 1>>
static A make_A() {
return A(value);
}
};
A
类型的对象将使用 A::make_A<0>()
创建。
这不是一个现代解决方案,但添加到 Zach Peltzer 的解决方案中,如果您使用宏,您可以保留您的语法...
template <int value, typename = std::enable_if_t<value == 0 | value == 1>>
constexpr int f_impl() {
// Do something with value
return 1;
}
#define f(x) f_impl<x>()
int main() {
f(0); //ok
f(1); //ok
f(2); //compile time error
}
不过,对于构造函数问题,您可以将 class 模板化,而不是尝试使用模板化构造函数
template<int value, typename = std::enable_if_t<value == 0 | value == 1>>
class A {
public:
A() {
//do stuff
}
};
int main() {
A<0> a0;
auto a1 = A<1>();
// auto a2 = A<2>(); //fails!
}
嗯...你已经标记了 C++17,所以你可以使用 if constexpr
。
所以当0_x
是一个std::integral_constant<int, 0>
值,当1_x
是一个std::integral_constant<int, 1>
,当2_x
(以及其他值)给出编译错误。
举例
template <char ... Chs>
auto operator "" _x()
{
using t0 = std::integer_sequence<char, '0'>;
using t1 = std::integer_sequence<char, '1'>;
using tx = std::integer_sequence<char, Chs...>;
if constexpr ( std::is_same_v<t0, tx> )
return std::integral_constant<int, 0>{};
else if constexpr ( std::is_same_v<t1, tx> )
return std::integral_constant<int, 1>{};
}
int main ()
{
auto x0 = 0_x;
auto x1 = 1_x;
//auto x2 = 2_x; // compilation error
static_assert( std::is_same_v<decltype(x0),
std::integral_constant<int, 0>> );
static_assert( std::is_same_v<decltype(x1),
std::integral_constant<int, 1>> );
}
现在你的f()
函数可以
template <int X, std::enable_if_t<(X == 0) || (X == 1), bool> = true>
void f (std::integral_constant<int, X> const &)
{
// do something with X
}
你可以这样调用它
f(0_x);
f(1_x);
在 C++20 中,您可以使用 consteval
关键字强制编译时求值。有了它,您可以创建一个结构,它有一个 consteval
构造函数并将其用作函数的参数。像这样:
struct S
{
private:
int x;
public:
S() = delete;
consteval S(int _x)
: x(_x)
{
if (x != 0 && x != 1)
{
// this will trigger a compile error,
// because the allocation is never deleted
// static_assert(_x == 0 || _x == 1); didn't work...
new int{0};
}
}
int get_x() const noexcept
{
return x;
}
};
void func(S s)
{
// use s.get_x() to decide control flow
}
int main()
{
func(0); // this works
func(1); // this also works
func(2); // this is a compile error
}
这里还有一个godbolt example。
编辑:
显然 clang 10
不会像 here 那样给出错误,但是 clang (trunk)
在 godbolt 上会出现错误。
有一个基本问题。您如何才能在编译器中为一个参数完成这些工作,同时又高效呢?那么,您到底需要什么?
这包含在 Pascal 或 Ada 等强类型语言中。枚举类型只有几个值,通常在开发时检查类型,但除此之外,检查会在 运行 时被某些编译器选项消除,因为 一切顺利.
一个函数接口就是一个契约。它是卖方(功能的编写者)和买方(该功能的用户)之间的合同。甚至还有一种仲裁程序,即编程语言,如果有人试图欺骗合约,它可以采取行动。但最后,该程序 运行 在一台开放的机器中进行任意操作,例如修改枚举值集并放置一个完整的(且不允许的值)。
单独编译也有问题。单独编译有它的缺点,因为它必须面对一个编译,而不必重新检查和重新测试你以前所做的所有编译。编译完成后,您放入代码中的所有内容都在那里。如果你希望代码高效,那么测试是多余的,因为调用者和实现者都处理契约,但如果你想抓住一个谎言,那么你必须包含测试代码。然后,对所有情况都做一次更好,还是让程序员决定什么时候和什么时候不想抓 lyer 更好?
C 的问题(以及 C++ 的遗留问题)是他们受到非常优秀的程序员的启发,他们没有错误,并且必须 运行 他们的软件在大而慢的机器上。他们决定使这两种语言(第二种语言是为了互操作性目的)成为弱类型的……它们就是这样。你试过用 Ada 编程吗?还是 Modula-2?你会发现,随着时间的推移,强类型的东西比其他东西更学术,最后,作为专业人士,你想要的是自由地说:现在我想要安全(并包括测试代码) ,现在我知道我在做什么了(请尽可能高效)
结论
结论是你可以自由select语言,select编译器,放宽规则。编译器有可能允许你这样做。你必须应对它,或者发明(这是今天几乎每周都会发生的事情)你自己的编程语言。
对于 Ada,您可以定义子类型、新类型或仅受整数 0 和 1 值约束的派生类型。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure two_value is
-- You can use any one of the following 3 declarations. Just comment out other two.
--subtype zero_or_one is Integer range 0 .. 1; -- subtype of Integer.
--type zero_or_one is range 0 .. 1; -- new type.
type zero_or_one is new Integer range 0 .. 1; -- derived type from Integer.
function get_val (val_1 : in zero_or_one) return Integer;
function get_val (val_1 : in zero_or_one) return Integer is
begin
if (val_1 = 0) then
return 0;
else
return 1;
end if;
end get_val;
begin
Put_Line("Demonstrate the use of only two values");
Put_Line(Integer'Image(get_val(0)));
Put_Line(Integer'Image(get_val(1)));
Put_Line(Integer'Image(get_val(2)));
end two_value;
编译时您会收到以下警告消息,尽管编译成功:
>gnatmake two_value.adb
gcc -c two_value.adb
two_value.adb:29:40: warning: value not in range of type "zero_or_one" defined at line 8
two_value.adb:29:40: warning: "Constraint_Error" will be raised at run time
gnatbind -x two_value.ali
gnatlink two_value.ali
执行它会出现编译器指定的运行时错误
>two_value.exe
Demonstrate the use of only two values
0
1
raised CONSTRAINT_ERROR : two_value.adb:29 range check failed
所以,基本上你可以通过定义新类型、派生类型或子类型来约束值,你不需要包含检查范围的代码,但编译器会根据你的数据类型自动警告你.