C++的隐藏特性是什么?

114

在讨论“隐藏功能”问题时,没有关于C++的爱吗?我想我应该提出这个问题。C++有哪些隐藏功能?


@Devtron - 我曾见过一些被当作功能出售的神奇漏洞(如意外行为)。事实上,游戏行业现在正在尝试让这种情况发生,并称之为“自发游戏”(此外,请查看《灵能追击》中的“TK冲浪”,它最初只是一个漏洞,但他们将其保留并成为了游戏中最好的功能之一,在我看来)。 - Grant Peters
5
并不是很多人从头到尾读过786页的ISO C++标准,但我想你肯定做到了,并且还记住了全部内容,对吗? - j_random_hacker
2
@Laith,@j_random:请查看我的问题“程序员的笑话是什么,我如何识别它,以及适当的回应”在http://stackoverflow.com/questions/1/you-have-been-link-rolled。 - Roger Pate
请查看 http://meta.stackexchange.com/questions/56669/should-hidden-features-of-x-be-removed-closed-locked,http://meta.stackexchange.com/questions/57226/should-we-have-a-list-of-x-close-reason 以及相关的 Meta 帖子。 - Roger Pate
64个回答

308

大部分 C++ 程序员都熟悉三目运算符:

x = (y < 0) ? 10 : 20;

然而,他们并没有意识到它可以被用作 lvalue:

(a == 0 ? a : b) = 1;

这是一个简写,意为

if (a == 0)
    a = 1;
else
    b = 1;

请小心使用 :-)


11
非常有趣。我可以看出那会造成一些难以阅读的代码。 - Jason Baker
112
哎呀。如果a等于0,那么(a == 0 ? a : b)会被赋值为(y < 0 ? 10 : 20)。 - Jasper Bekkers
52
(b ? trueCount : falseCount)++ 的意思是根据条件 b 的真假情况,将 trueCount 或 falseCount 中的一个加一,并返回加一后的结果。 - Pavel Radzivilovsky
12
不确定这是否适用于GCC,但我惊讶地发现这也起作用:(value ? function1 : function2)() - Chris Burt-Brown
3
不,如果它们具有相同的类型(即没有默认参数),那么在任何地方都应该可以工作。 function1function2会被隐式转换为函数指针,然后将结果隐式转换回去。 - MSalters
显示剩余4条评论

238

您可以在C++源代码中放置URI而不会出错。例如:

void foo() {
    http://stackoverflow.com/
    int bar = 4;

    ...
}

41
但是每个功能只能有一个,我猜是这样吧? :) - Constantin
51
@jpoh说:在后面加上冒号的http成为一个“标签”,您稍后可以在goto语句中使用它。编译器发出警告是因为在上面的示例中它没有在任何goto语句中使用过。 - utku_karatas
9
只要使用不同的协议,您可以添加多个!例如:ftp://ftp.microsoft.com、gopher://aerv.nl/1 等等... - Daniel Earwicker
4
一个紧跟着冒号的标识符是一个标签(用于 goto 语句,在 C++ 中可以使用)。两个斜杠后面的任何内容都是注释。因此,在 http://stackoverflow.com 中,http 是一个标签(理论上您可以编写 goto http;),而 //stackoverflow.com 只是一条行末注释。这两者都是合法的 C++ 代码,所以这个结构可以编译通过。当然,它并没有做任何有用的事情。 - David Thornley
8
很遗憾,“goto http;”并不能跳转到指定的URL。 :( - Yakov Galka
显示剩余6条评论

140

指针算术。

C++程序员喜欢避免使用指针,因为可能会引入错误。

我见过的最酷的C++是什么?模拟字面量。


11
我们避免使用指针是因为它容易出现错误?但指针基本上是动态C++编程的全部! - Nick Bedford
1
模拟字面量非常适合用于混淆的C++竞赛条目,特别是ASCII艺术类型。 - Synetech

119

我同意大多数帖子的观点:C++是一种多范式语言,所以你会发现“隐藏”的功能(除了应该尽量避免的“未定义行为”之外)都是设施的巧妙使用。

这些设施中的大部分都不是语言内置功能,而是基于库的。

最重要的是RAII,它经常被来自C世界的C++开发人员忽视多年。 运算符重载通常是一个被误解的功能,它可以实现类似数组的行为(下标运算符),指针般的操作(智能指针)和类似内置操作(矩阵乘法)。

使用异常通常很困难,但通过一些工作,可以通过异常安全规范产生非常健壮的代码(包括不会失败的代码,或者具有提交样式功能的代码,即将成功或恢复到其原始状态)。

C++最著名的“隐藏”功能是模板元编程,因为它使您能够在编译时部分(或完全)执行程序,而不是在运行时执行。不过,这很困难,您必须在尝试之前对模板有牢固的掌握。

其他使用多范式的方式产生“编程方式”超出了C++的祖先,即C。

通过使用函数对象,您可以模拟函数,并具有附加的类型安全性和状态。使用命令模式,您可以延迟代码执行。大多数其他设计模式都可以在C++中轻松高效地实现,以产生不应在“官方C++范例”列表中的替代编码风格。

通过使用模板,您可以生成适用于大多数类型(包括您最初想到的类型之外的类型)的代码。您还可以增加类型安全性,就像自动类型安全的malloc / realloc / free一样。C ++对象特性非常强大(因此,如果不小心使用,可能很危险),但是即使在C ++中,动态多态性也有其静态版本:CRTP。

我发现Scott Meyers的大多数“Effective C ++”类型的书籍或Herb Sutter的“Exceptional C ++”类型的书籍都很容易阅读,并且对C ++的已知和未知特性提供了宝贵的信息。

我最喜欢的之一应该会让任何Java程序员感到恐惧:在C ++中,将功能添加到对象的最面向对象的方法是通过非成员非友元函数,而不是成员函数(即类方法),因为:

  • 在C ++中,类的接口既是其成员函数,也是同一命名空间中的非成员函数

  • 非友元非成员函数无权访问类内部。因此,使用成员函数而不是非成员非友元函数将削弱类的封装性。

即使是经验丰富的开发人员也会感到惊讶。

(来源:包括Herb Sutter的在线Guru of the Week#84:http://www.gotw.ca/gotw/084.htm


+1 非常详尽的回答。显然由于未完全公开(否则就不会有“隐藏功能”了!):p在回答的第一点中,您提到了类接口的成员。您是指“...既包括其成员函数,也包括友元非成员函数”吗? - wilhelmtell
CRTP - http://en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern CRTP - http://zh.wikipedia.org/wiki/奇特递归模板模式 - J.J.
你所提到的1是指Koenig查找,对吗? - Özgür
1
@wilhelmtell:不不不... :-p ... 我的意思是“它的成员函数和非友元非成员函数”.... Koenig的查找将确保这些函数在其符号搜索中比其他“外部”函数更早地被考虑。 - paercebal
@Comptrol:你几乎是对的,这是Keonig查找的结果 - paercebal
7
好的文章,特别是最后一部分,值得点赞。我可能还会将Boost库添加为“隐藏功能”。我认为它几乎可以视为C++应该拥有的标准库。 ;) - jalf

118

我认为比较隐蔽的一种编程语言特性是命名空间别名。在我的学校生涯中从未听到过这方面的内容,直到我在boost文档中遇到了相关示例才引起了我的注意。当然,现在我知道了这个特性,你可以在任何标准的C++参考资料中找到它。

namespace fs = boost::filesystem;

fs::path myPath( strPath, fs::native );

1
如果你不想使用 using,我猜这会很有用。 - Siqi Lin
4
这也可以作为在不同实现之间切换的一种有用方式,比如选择线程安全还是非线程安全,或者版本1与版本2之间的切换。 - Tony Delroy
3
如果你正在处理一个命名空间层次结构很大的大型项目,并且不想让你的头文件造成命名空间污染(同时你希望你的变量声明易于阅读),那么这将非常有用。 - Brandon Bohrer

102

for 循环的初始化部分不仅可以声明变量,还可以声明类和函数。

for(struct { int a; float b; } loop = { 1, 2 }; ...; ...) {
    ...
}

这允许使用不同类型的多个变量。


31
很高兴知道你能够做到,但个人认为最好尽量避免这样做。主要是因为它难以阅读。 - Zoomulator
2
实际上,在这种情况下,可以使用一对元素:for ( std::pair<int,float> loop=std::make_pair(1,2); loop.first > 0; loop.second+=1) - Valentin H
2
@Valentin,那我建议你尝试向VS2008提交一个错误报告,而不是对这个隐藏功能进行贬低评价。显然这是你的编译器的问题。 - Johannes Schaub - litb
2
哎呀,在msvc10中也不起作用。太遗憾了 :( - avakar
2
@avakar,事实上,gcc引入了一个bug,在v4.6中也会拒绝它 :) 请参见http://gcc.gnu.org/bugzilla/show_bug.cgi?id=46791 - Johannes Schaub - litb
显示剩余3条评论

77

数组操作符是可结合的。

A[8] 是 *(A + 8) 的同义词。由于加法是可结合的,这可以重写为*(8 + A),它是... 8[A] 的同义词。

你没有说这很有用... :-)


15
实际上,当使用这个技巧时,你需要特别注意你使用的类型。A[8]实际上是第8个A,而8[A]是从地址为8开始的第Ath个整数。如果A是一个字节,那么你就有一个错误。 - Vincent Robert
38
你的意思是当你说“可交换”时,实际上想表达的是“结合律”吗? - DarenW
28
文森特,你错了。A 的类型根本不重要。例如,如果 A 是一个 char*,那么这段代码仍然是有效的。 - Konrad Rudolph
11
注意,A必须是一个指针,而不是一个重载了运算符[]的类。 - David Rodríguez - dribeas
15
在这里,必须有一个整型和一个指针类型,C和C++都不关心哪个先出现。 - David Thornley
2
A[8] 的值为 *(A + 8)。而 8[A] 的值为 *(8 + A)。它们是相同的。 - Marlon

73

有一件不太为人知的事情是,union 也可以作为模板:

template<typename From, typename To>
union union_cast {
    From from;
    To   to;

    union_cast(From from)
        :from(from) { }

    To getTo() const { return to; }
};

它们也可以拥有构造函数和成员函数。只是不能涉及继承(包括虚函数)。


这难道不会引起未定义行为吗? - Greg Bacon
7
是的,如果将 FromTo 设置并相应使用,则会触发未定义行为。但是,这样的 union 可以通过特定方式(使用 To 作为无符号字符数组或共享与 From 相同初始序列的结构体)来实现定义良好的行为。即使以未定义的方式使用它,它仍可能对低级别工作有用。无论如何,这只是一个联合模板的示例 - 对于联合模板可能还有其他用途。 - Johannes Schaub - litb
3
构造函数需要注意。请注意您只需要构造第一个元素,并且这仅在C++0x中允许。根据当前标准,您必须坚持使用可平凡构造类型。此外,不能有析构函数。 - Potatoswatter
所以基本上你是这样做的吗? *(B *)&a - Nick Bedford
@JohannesSchaub-litb:此外,位域可以进行模板化:https://dev59.com/BF3Va4cB1Zd3GeqPDb4B#8309592 - sehe
显示剩余3条评论

72

C++是一个标准,不应该有任何隐藏功能...

C++是一种多范式语言,你可以打赌最后的钱里面有隐藏功能。其中一个例子是模板元编程。标准委员会中没有人打算在编译时执行一个图灵完备的子语言。


65

另一个在C语言中不起作用的隐藏功能是一元+运算符的功能。您可以使用它来提升和降低各种事物。

将枚举转换为整数

+AnEnumeratorValue

现在你的枚举值已经具有完美的整数类型,可以容纳其值。手动操作时,你几乎不会知道该类型!例如,当你想为枚举实现重载运算符时,就需要这样做。

从变量中获取值

你必须使用一个类,该类使用内部静态初始化程序而没有外部定义,但有时链接失败?该运算符可以帮助创建一个临时变量,而不对其类型进行任何假设或依赖。

struct Foo {
  static int const value = 42;
};

// This does something interesting...
template<typename T>
void f(T const&);

int main() {
  // fails to link - tries to get the address of "Foo::value"!
  f(Foo::value);

  // works - pass a temporary value
  f(+Foo::value);
}

将数组衰变为指针

您想将两个指针传递给函数,但它却无法工作?运算符可能会有所帮助。

// This does something interesting...
template<typename T>
void f(T const& a, T const& b);

int main() {
  int a[2];
  int b[3];
  f(a, b); // won't work! different values for "T"!
  f(+a, +b); // works! T is "int*" both time
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接