没有合适的资源?快使用搜索试试~ 我知道了~
C++ FQA Lite
5星 · 超过95%的资源 需积分: 9 14 下载量 160 浏览量
2012-07-25
14:42:48
上传
评论
收藏 1.01MB PDF 举报
温馨提示
试读
98页
请注意,不是C++ FAQ,而是C++ FQA。 是讲C++语言本身缺点的(C++不足之处)。 原文出处:http://yosefk.com/c++fqa/
资源推荐
资源详情
资源评论
C++ Frequently Questioned Answers
This is a single page version of C++ FQA Lite. C++ is a general-
purpose programming language, not necessarily suitable for your
special purpose. FQA stands for "frequently questioned answers". This FQA is called "lite" because it questions the answers
found in C++ FAQ Lite.
The single page version does not include most "metadata" sections such as the FQA FAQ.
Table of contents
Defective C++ - a list of major language defects
C++ Q&A, structured similarly to C++ FAQ Lite, with links to the original FAQ answers
Big Picture Issues
Classes and objects
Inline functions
References
Constructors
Destructors
Assignment operators
Operator overloading
Friends
Input/output via
<iostream>
and
<cstdio>
Freestore management
Exceptions
Const correctness
Inheritance - basics
Inheritance -
virtual
functions
Inheritance - proper inheritance and substitutability
Inheritance - abstract base classes
Inheritance - what your mother never told you
Inheritance - multiple and
virtual
inheritance
How to mix C and C++
Pointers to member functions
Templates
FQA errors found by readers
Defective C++
This page summarizes the major defects of the C++ programming language (listing all minor quirks would take eternity). To be
fair, some of the items by themselves could be design choices, not bugs. For example, a programming language doesn't have to
provide garbage collection. It's the combination of the things that makes them all problematic. For example, the lack of garbage
collection makes C++ exceptions and operator overloading inherently defective. Therefore, the problems are not listed in the
order of "importance" (which is subjective anyway - different people are hit the hardest by different problems). Instead, most
defects are followed by one of their complementary defects, so that when a defect causes a problem, the next defect in the list
makes it worse.
No compile time encapsulation
Outstandingly complicated grammar
No way to locate definitions
No run time encapsulation
No binary implementation rules
No reflection
Very complicated type system
Very complicated type-based binding rules
Defective operator overloading
Defective exceptions
Duplicate facilities
No high-level built-in types
Manual memory management
Defective metaprogramming facilities
Unhelpful standard library
Defective inlining
Implicitly called & generated functions
No compile time encapsulation
In naturally written C++ code, changing the private members of a class requires recompilation of the code using the class. When
the class is used to instantiate member objects of other classes, the rule is of course applied recursively.
This makes C++ interfaces very unstable - a change invisible at the interface level still requires to rebuild the calling code, which
can be very problematic when that code is not controlled by whoever makes the change. So shipping C++ interfaces to
Page
1
of
98
customers can be a bad idea.
Well, at least when all relevant code is controlled by the same team of people, the only problem is the frequent rebuilds of large
parts of it. This wouldn't be too bad by itself with almost any language, but C++ has...
Outstandingly complicated grammar
"Outstandingly" should be interpreted literally, because all popular languages have context-free (or "nearly" context-free)
grammars, while C++ has undecidable grammar. If you like compilers and parsers, you probably know what this means. If you're
not into this kind of thing, there's a simple example showing the problem with parsing C++: is
AA BB(CC);
an object definition or a
function declaration? It turns out that the answer depends heavily on the code before the statement - the "context". This shows
(on an intuitive level) that the C++ grammar is quite context-sensitive.
In practice, this means three things. First, C++ compiles slowly (the complexity takes time to deal with). Second, when it doesn't
compile, the error messages are frequently incomprehensible (the smallest error which a human reader wouldn't notice
completely confuses the compiler). And three, parsing C++ right is very hard, so different compilers will interpret it differently, and
tools like debuggers and IDEs periodically get awfully confused.
And
slow compilation interacts badly with frequent recompilation. The latter is caused by the lack of encapsulation mentioned
above, and the problem is amplified by the fact that C++ has...
No way to locate definitions
OK, so before we can parse
AA BB(CC);
, we need to find out whether
CC
is defined as an object or a type. So let's locate the
definition of
CC
and move on, right?
This would work in most modern languages, in which
CC
is either defined in the same module (so we've already compiled it), or it
is imported from another module (so either we've already compiled it, too, or this must be the first time we bump into that module
- so let's compile it now, once, but of course not the next time we'll need it). So to compile a program, we need to compile each
module, once, no matter how many times each module is used.
In C++, things are different - there are no modules. There are files, each of which can contain many different definitions or just
small parts of definitions, and there's no way to tell in which files
CC
is defined, or which files must be parsed in order to
"understand" its definition. So who is responsible to arrange all those files into a sensible string of C++ code? You, of course! In
each compiled file, you
#include
a bunch of header files (which themselves include other files); the
#include
directive basically
issues a copy-and-paste operation to the C preprocessor, inherited by C++ without changes. The compiler then parses the result
of all those copy-and-paste operations. So to compile a program, we need to compile each file the number of times it is used in
other files.
This causes two problems. First, it multiplies the long time it takes to compile C++ code by the number of times it's used in a
program. Second, the only way to figure out what should be recompiled after a change to the code is to check which of the
#include
files have been changed since the last build. The set of files to rebuild generated by this inspection is usually a superset
of the files that really must be recompiled according to the C++ rules of dependencies between definitions. That's because most
files
#include
definitions they don't really need, since people can't spend all their time removing redundant inclusions.
Some compilers support "precompiled headers" - saving the result of the parsing of "popular" header files to some binary file and
quickly loading it instead of recompiling from scratch. However, this only works well with definitions that almost never change,
typically third-party libraries.
And now that you've waited all that time until your code base recompiles, it's time to run and test the program, which is when the
next problem kicks in.
No run time encapsulation
Programming languages have rules defining "valid" programs - for example, a valid program shouldn't divide by zero or access
the 7th element of an array of length 5. A valid program isn't necessarily correct (for example, it can delete a file when all you
asked was to move it). However, an invalid program is necessarily incorrect (there is no 7th element in the 5-element array). The
question is, what happens when an invalid program demonstrates its invalidity by performing a meaningless operation?
If the answer is something like "an exception is raised", your program runs in a managed environment. If the answer is "anything
can happen", your program runs somewhere else. In particular, C and C++ are not designed to run in managed environments
(think about pointer casts), and while in theory they could run there, in practice all of them run elsewhere.
So what happens in a C++ program with the 5-element array? Most frequently, you access something at the address that would
contain the 7th element, but since there isn't any, it contains something else, which just happens to be located there. Sometimes
you can tell from the source code what that is, and sometimes you can't. Anyway, you're really lucky if the program crashes;
because if it keeps running, you'll have hard time understanding why it ends up crashing or misbehaving later. If it doesn't scare
you (you debugged a couple of buffer overflows and feel confident), wait until you get to many megabytes of machine code and
many months of execution time. That's when the real fun starts.
Now, the ability of a piece of code to modify a random object when in fact it tries to access an unrelated array indicates that
C++
has no run time encapsulation. Since it doesn't have compile time encapsulation, either, one can wonder why it calls itself object-
Page
2
of
98
oriented. Two possible answers are warped perspective and marketing (these aren't mutually exclusive).
But if we leave the claims about being object-oriented aside, the fact that a language runs in unmanaged environments can't
really be called a "bug". That's because managed environments check things at run time to prevent illegal operations, which
translates to a certain (though frequently overestimated) performance penalty. So when performance isn't that important, a
managed environment is the way to go. But when it's critical, you just have to deal with the difficulties in debugging. However,
C++ (compared to C, for example) makes that much harder that it already has to be, because there are...
No binary implementation rules
When an invalid program finally crashes (or enters an infinite loop, or goes to sleep forever), what you're left with is basically the
binary snapshot of its state (a common name for it is a "core dump"). You have to make sense of it in order to find the bug.
Sometimes a debugger will show you the call stack at the point of crash; frequently that information is overwritten by garbage.
Other things which can help the debugger figure things out may be overwritten, too.
Now, figuring out the meaning of partially corrupted memory snapshots is definitely not the most pleasant way to spend one's
time. But with unmanaged environments you have to do it and it can be done, if you know how your source code maps to binary
objects and code. Too bad that with C++, there's a ton of these rules and each compiler uses different ones. Think about
exception handling or various kinds of inheritance or virtual functions or the layout of standard library containers. In C, there's no
standard binary language implementation rules, either, but it's an order of magnitude simpler and in practice compilers use the
same rules. Another reason making C++ code hard to debug is the above-mentioned complicated grammar, since debuggers
frequently can't deal with many language features (place breakpoints in templates, parse pointer casting commands in data
display windows, etc.).
The lack of a standard ABI (application binary interface) has another consequence - it makes shipping C++ interfaces to other
teams / customers impractical since the user code won't work unless it's compiled with the same tools and build options. We've
already seen another source of this problem - the instability of binary interfaces due to the lack of compile time encapsulation.
The two problems - with debugging C++ code and with using C++ interfaces - don't show up until your project grows complicated
in terms of code and / or human interactions, that is, until it's too late. But wait, couldn't you deal with both problems
programmatically? You could generate C or other wrappers for C++ interfaces and write programs automatically shoveling
through core dumps and deciphering the non-corrupted parts, using something called reflection. Well, actually, you couldn't, not
in a reasonable amount of time - there's...
No reflection
It is impossible to programmatically iterate over the methods or the attributes or the base classes of a class in a portable way
defined by the C++ standard. Likewise, it is impossible to programmatically determine the type of an object (for dynamically
allocated objects, this can be justified to an extent by performance penalties of RTTI, but not for statically allocated globals, and if
you could start at the globals, you could decipher lots of memory pointed by them). Features of this sort - when a program can
access the structure of programs, in particular, its own structure - are collectively called reflection, and C++ doesn't have it.
As mentioned above, this makes generating wrappers for C++ classes and shoveling through memory snapshots a pain, but
that's a small fraction of the things C++ programmers are missing due to this single issue. Wrappers can be useful not only to
work around the problem of shipping C++ interfaces
- you could automatically handle things like remote procedure calls, logging
method invocations, etc. A very common application of reflection is serialization - converting objects to byte sequences and vice
versa. With reflection, you can handle it for all types of objects with the same code - you just iterate over the attributes of
compound objects, and only need special cases for the basic types. In C++, you must maintain serialization-related code and/or
data structures for every class involved.
But perhaps we could deal with this problem programmatically then? After all, debuggers do manage to display objects somehow
- the debug information, emitted in the format supported by your tool chain, describes the members of classes and their offsets
from the object base pointer and all that sort of meta-data. If we're stuck with C++, perhaps we could parse this information and
thus have non-standard, but working reflection? Several things make this pretty hard - not all compilers can produce debug
information and
optimize the program aggressively enough for a release build, not all debug information formats are documented,
and then in C++, we have a...
Very complicated type system
In C++, we have standard and compiler-specific built-in types, structures, enumerations, unions, classes with single, multiple,
virtual and non-virtual inheritance,
const
and
volatile
qualifiers, pointers, references and arrays,
typedef
s, global and member
functions and function pointers, and templates, which can have specializations on (again) types (or integral constants), and you
can "partially specialize" templates by pattern matching their type structure (for example, have a specialization for
std::vector<MyRetardedTemplate<T> >
for arbitrary values of
T
), and each template can have base classes (in particular, it can
be derived from its own instantiations recursively, which is a well-known practice documented in books), and inner
typedef
s,
and... We have lots of kinds of types.
Naturally, representing the types used in a C++ program, say, in debug information, is not an easy task. A trivial yet annoying
manifestation of this problem is the expansion of
typedef
s done by debuggers when they show objects (and compilers when they
produce error messages - another reason why these are so cryptic). You may think it's a
StringToStringMap
, but only until the
tools enlighten you - it's actually more of a...
Page
3
of
98
// don't read this, it's impossible. just count the lines
std::map<std::basic_string<char, std::char_traits<char>, std::allocator<char> >,
std::basic_string<char, std::char_traits<char>, std::allocator<char> >,
std::less<std::basic_string<char, std::char_traits<char>, std::allocator<char> >
>, std::allocator<std::pair<std::basic_string<char, std::char_traits<char>,
std::allocator<char> > const, std::basic_string<char, std::char_traits<char>,
std::allocator<char> > > > >
But wait, there's more! C++ supports a wide variety of explicit and implicit type conversions, so now we have a nice set of rules
describing the cartesian product of all those types, specifically, how conversion should be handled for each pair of types. For
example, if your function accepts
const std::vector<const char*>&
(which is supposed to mean "a reference to an immutable
vector of pointers to immutable built-in strings"), and I have a
std::vector<char*>
object ("a mutable vector of mutable built-in
strings"), then I can't pass it to your function because the types aren't convertible. You have to admit that it doesn't make any
sense, because your function guarantees that it won't change anything, and I guarantee that I don't even mind having anything
changed, and still the C++ type system gets in the way and the only sane workaround is to copy the vector. And this is an
extremely simple example - no virtual inheritance, no user-defined conversion operators, etc.
But conversion rules by themselves are still not the worst problem with the complicated type system. The worst problem is the...
Very complicated type-based binding rules
Types lie at the core of the C++ binding rules. "Binding" means "finding the program entity corresponding to a name mentioned in
the code". When the C++ compiler compiles something like
f(a,b)
(or even
a+b
), it relies on the argument types to figure out
which version of
f
(or
operator+
) to call. This includes overload resolution (is it
f(int,int)
or
f(int,double)
?), the handling of
function template specializations (is it
template<class T> void f(vector<T>&,int)
or
template<class T> void f(T,double)
?),
and the argument-dependent lookup (ADL) in order to figure out the namespace (is it
A::f
or
B::f
?).
When the compiler "succeeds" (translates source code to object code), it doesn't mean that you are equally successful (that is,
you think
a+b
called what the compiler thought it called). When the compiler "fails" (translates source code to error messages),
most humans also fail (to understand these error messages; multiple screens listing all available overloads of things like
operator<<
are less than helpful). By the way, the C++ FAQ has very few items related to the unbelievably complicated static
binding, like overload resolution or ADL or template specialization. Presumably people get too depressed to ask any questions
and silently give up.
In short, the complicated type system interacts very badly with overloading - having multiple functions with the same name and
having the compiler figure out which of them to use based on the argument types (don't confuse it with overriding -
virtual
functions, though very far from perfect, do follow rules quite sane by C++ standards). And probably the worst kind of overloading
is...
Defective operator overloading
C++ operator overloading has all the problems of C++ function overloading (incomprehensible overload resolution rules), and
then some. For example, overloaded operators have to return their results by value - naively returning references to objects
allocated with
new
would cause temporary objects to "leak" when code like
a+b+c
is evaluated. That's because C++ doesn't have
garbage collection, since that, folks, is inefficient. Much better to have your code copy massive temporary objects and hope to
have them optimized out by our friend the clever compiler. Which, of course, won't happen any time soon.
Like several other features in C++, operator overloading is not necessarily a bad thing by itself - it just happens to interact really
badly with other things C++. The lack of automatic memory management is one thing making operator overloading less than
useful. Another such thing is...
Defective exceptions
Consider error handling in an overloaded operator or a constructor. You can't use the return value, and setting/reading error flags
may be quite cumbersome. How about throwing an exception?
This could be a good idea in some cases if C++ exceptions were any good. They aren't, and can't be - as usual, because of
another C++ "feature", the oh-so-efficient manual memory management. If we use exceptions, we have to write exception-safe
code - code which frees all resources when the control is transferred from the point of failure (
throw
) to the point where explicit
error handling is done (
catch
). And the vast majority of "resources" happens to be memory, which is managed manually in C++.
To solve this, you are supposed to use RAII, meaning that all pointers have to be "smart" (be wrapped in classes freeing the
memory in the destructor, and then you have to design their copying semantics, and...). Exception safe C++ code is almost
infeasible to achieve in a non-trivial program.
Of course, C++ exceptions have other flaws, following from still other C++ misfeatures. For example, the above-
mentioned lack of
reflection in the special case of exceptions means that when you catch an exception, you can't get the call stack describing the
context where it was thrown. This means that debugging illegal pointer dereferencing may be easier than figuring out why an
exception was thrown, since a debugger will list the call stack in many cases of the former.
At the bottom line,
throw/catch
are about as useful as
longjmp/setjmp
(BTW, the former typically runs faster, but it's mere
existence makes the rest of the code run slower, which is almost never acknowledged by C++ aficionados). So we have two
features, each with its own flaws, and no interoperability between them. This is true for the vast majority of C++ features - most
are...
Page
4
of
98
Duplicate facilities
If you need an array in C++, you can use a C-like
T arr[]
or a C++
std::vector<T>
or any of the array classes written before
std::vector
appeared in the C++ standard. If you need a string, use
char*
or
std::string
or any of the pre-standard string
classes. If you need to take the address of an object, you can use a C-like pointer,
T*
, or a C++ reference,
T&
. If you need to
initialize an object, use C-like aggregate initialization or C++ constructors. If you need to print something, you can use a C-like
printf
call or a C++
iostream
call. If you need to generate many similar definitions with some parameters specifying the
differences between them, you can use C-like macros or C++ templates. And so on.
Of course you can do the same thing in many ways in almost any language. But the C++ feature duplication is quite special. First,
the many ways to do the same thing are usually not purely syntactic options directly supported by the compiler -
you can compute
a+b
with
a-b*-1
, but that's different from having
T*
and
T&
in the same language. Second, you probably noticed a pattern - C++
adds features duplicating functionality already in C. This is bad by itself, because the features don't interoperate well (you can't
printf
to an
iostream
and vice versa, code mixing
std::string
and
char*
is littered with casts and calls to
std::string::c_str
,
etc.). This is made even worse by the pretty amazing fact that the new C++ features are actually inferior to the old C ones in
many aspects.
And the best part is that C++ devotees
dare to refer to the C features as evil, and frequently will actually resort to finger pointing
and name calling when someone uses them in C++ code (not to mention using plain C)! And at the same time they (falsely
) claim
that C++ is compatible with C and it's one of its strengths (why, if C is so evil?). The real reason to leave the C syntax in C++ was
of course marketing - there's absolutely NO technical reason to parse C-like syntax in order to work with existing C code since
that code can be compiled separately. For example, mixing C and the D programming language isn't harder than mixing C and
C++. D is a good example since its stated goals are similar to those of C++, but almost all other popular languages have ways to
work with C code.
So IMO all that old syntax was kept for strictly commercial purposes - to market the language to non-technical managers or
programmers who should have known better and didn't understand the difference between "syntax" and "compatibility with
existing code" and simply asked whether the old code will compile with this new compiler. Or maybe they thought it would be
easier to learn a pile of new syntax when you also have the (smaller) pile of old syntax than when you have just the new syntax.
Either way, C++ got wide-spread by exploiting misconceptions.
Well, it doesn't matter anymore why they kept the old stuff. What matters is that the new stuff isn't really new, either - it's
obsessively built in ways exposing the C infrastructure underneath it. And that
is purely a wrong design decision, made without an
axe to grind. For example, in C++ there's...
No high-level built-in types
C is a pretty low-
level language. Its atomic types are supposed to fit into machine registers (usually one, sometimes two of them).
The compound types are designed to occupy a flat chunk of memory with of a size known at compile time.
This design has its virtues. It makes it relatively easy to estimate the performance & resource consumption of code. And when
you have hard-to-catch low-level bugs, which sooner or later happens in unmanaged environments, having a relatively simple
correspondence between source code definitions and machine memory helps to debug the problem. However, in a high-level
language, which is supposed to be used when the development-time-cost / execution-time-cost ratio is high, you need things like
resizable arrays, key-value mappings, integers that don't overflow and other such gadgets. Emulating these in a low-level
language is possible, but is invariably painful since the tools don't understand the core types of your program.
C++ doesn't add any built-in types to C (correction). All higher-level types must be implemented as user-defined classes and
templates, and this is when the defects of C++ classes and templates manifest themselves in their full glory. The lack of syntactic
support for higher-level types (you can't initialize
std::vector
with
{1,2,3}
or initialize an
std::map
with something like
{"a":1,"b":2}
or have large integer constants like
3453485348545459347376
) is the small part of the problem. Cryptic multi-line or
multi-screen compiler error messages, debuggers that can't display the standard C++ types and slow build times unheard of
anywhere outside of the C++ world are the larger part of the problem. For example, here's a simple piece of code using the C++
standard library followed by an error message produced from it by gcc 4.2.0. Quiz: what's the problem?
// the code
typedef std::map<std::string,std::string> StringToStringMap;
void print(const StringToStringMap& dict) {
for(StringToStringMap::iterator p=dict.begin(); p!=dict.end(); ++p) {
std::cout << p->first << " -> " << p->second << std::endl;
}
}
// the error message
test.cpp: In function 'void print(const StringToStringMap&)':
test.cpp:8: error: conversion from
'std::_Rb_tree_const_iterator<std::pair<const std::basic_string<char,
std::char_traits<char>, std::allocator<char> >, std::basic_string<char,
std::char_traits<char>, std::allocator<char> > > >' to non-scalar type
'std::_Rb_tree_iterator<std::pair<const std::basic_string<char,
std::char_traits<char>, std::allocator<char> >, std::basic_string<char,
std::char_traits<char>, std::allocator<char> > > >' requested
The decision to avoid new built-in types yields other problems, such as the ability to throw anything, but without the ability to catch
it later.
class Exception
, a built-in base class for all exception classes treated specially by the compiler, could solve this problem
with C++ exceptions (but not others). However, the most costly problem with having no new high
-level built-in types is probably
Page
5
of
98
剩余97页未读,继续阅读
资源评论
- zippo9992014-09-17可以看看开阔一下
- yaofengzhuzhu2013-10-16C++进阶书籍,C++里面有很多不完美之处
- Takyne2013-02-22把C++ 逻辑层面的取舍都写出来了。
uconline
- 粉丝: 0
- 资源: 2
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
最新资源
- Screenshot_20240427_031602.jpg
- 网页PDF_2024年04月26日 23-46-14_QQ浏览器网页保存_QQ浏览器转格式(6).docx
- 直接插入排序,冒泡排序,直接选择排序.zip
- 在排序2的基础上,再次对快排进行优化,其次增加快排非递归,归并排序,归并排序非递归版.zip
- 实现了7种排序算法.三种复杂度排序.三种nlogn复杂度排序(堆排序,归并排序,快速排序)一种线性复杂度的排序.zip
- 冒泡排序 直接选择排序 直接插入排序 随机快速排序 归并排序 堆排序.zip
- 课设-内部排序算法比较 包括冒泡排序、直接插入排序、简单选择排序、快速排序、希尔排序、归并排序和堆排序.zip
- Python排序算法.zip
- C语言实现直接插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序、归并排序、计数排序,并带图详解.zip
- 常用工具集参考用于图像等数据处理
资源上传下载、课程学习等过程中有任何疑问或建议,欢迎提出宝贵意见哦~我们会及时处理!
点击此处反馈
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功