Effective C++ | 01 Accustom yourself to C++

文章讲述了C++中的多语言特性,提倡使用const、枚举和内联函数代替#define,强调const的正确使用,包括指针和对象的const修饰,以及const成员函数的重要性。同时,文章讨论了对象的初始化,尤其是静态对象在不同翻译单元中的初始化顺序问题,并提出了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Effective C++ | 01 Accustom yourself to C++

Book: Effective C++ (Third Edition)

Item 01: View C++ as a federation of languages

C++ is multiparadigm programming language:

  • procedural
  • object-orientated
  • functional
  • generic
  • metaprogramming features

C++ is not a single language.

C++ is a federation of languages:

  1. C
  2. Object-Orientated C++
  3. Template C++
  4. STL

Pass by value (C)
More efficient that pass by reference for built-in (C-like) types

Pass by reference to const (Object-Oriented, Template)
Better for user-defined objects (with constructors / destructors)
Esp. true for template C++ (you don’t know type you’re dealing)

Pass by value (STL)
Better for STL because iterators and function objects are modelled on pointers in C
Iterators and function objects in STL pass by value is better

Item 02: Prefer consts, enums, and inlines to #defines

Prefer compiler to the preprocessor

#define ASPECT_RATIO 1.653
const double AspectRatio = 1.653;

The latter is better.

AspectRatio seen by compilers and entered into symbol tables

Also 1.653 (#define) multiple times in object code
Whereas constant AspectRatio appears once

2x SPECIAL CASES

01.Defining constant pointers

Constant definitions in headers files means pointer declared const plus what pointer points to
const char* const authorName = “Scott Meyers”;

NB: string prob better than char*

const std:: authorName("Scott Meyers");

02.Class-specific constants

Limit scope of constant to class = make it a member
Ensure at most one copy of constant = make it static

// GamePlayer.h
class GamePlayer 
{
private:
	static const int NumTurns = 5;	// constant declaration
	int scores[NumTurns];
};

C++ requires definition for declaration except class-specific constants that are static of integral type
(int, char, bool)

NB: if you take address then compiler insists on definition:

// GamePlayer.cpp
const int GamePlayer::NumTurns; 	// definition	no value!

IMP
Cannot create class-specific constant using #define
#define does not respect scope: can’t be private

If (old) compiler does not allow initialization in declaration (above) then do this

// GamePlayer.h
class GamePlayer 
{
private:
	static const int NumTurns;	// constant declaration
};
// GamePlayer.cpp
const int GamePlayer::NumTurns = 5; 	// definition	no value!

However, now scores array won’t work 'cos you need the value of a class constant during compilation:
compilers insist on knowing the size of the array

Only way to forbid in-class specification of initial values for static integral class constants:

ENUM HACK

class GamePlayer
{
private:
  enum { NumTurns = 5 };        // "the enum hack" ?makes NumTurns a symbolic name for 5
  int scores[NumTurns];         // fine
};

Enum hack used a lot!
Acts more like #define than const

Why?
Take address of const——Legal
Take address of enum——Illegal
Take address of #define——Illegal

If you don’t want people get pointer or reference to integral constants then enum enforces that constraint

Also, enum hack is fundamental technique of template metaprogramming

MACROs
Macros look like functions but don’t incur overhead of function call
Arrgh : must parenthesize all arguments in the macro body!

Do not code like this:

// call f with the maximum of a and b
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))


int a = 5, b = 0;
CALL_WITH_MAX(++a, b);          // a is incremented twice
CALL_WITH_MAX(++a, b+10);       // a is incremented once

this is right:

template<typename T>
inline void callWithMax(const T& a, const T& b)
{
	f(a > b ? a : b);
}

No need to
parenthesize parameters inside function body
evaluate parameters multiple times

callWithMax obeys scope and access rules
Can inline function private to the class

Can’t do that with Macro!

SUMMARY

  • For simple constants, prefer const or enums to #define
  • For function-like macros, prefer inline functions to #define

Item 03: Use const whenever possible

An object should not be modified ?and compilers will enforce that constraint.

Pointers
Specify whether pointer is const, data it points to is const, both or neither

char greeting[] = "Hello";
char stevepro[] = "Steve";
char* p1 = greeting;	// neither const
const char* p2 = greeting;	// DATA const
char* const p3 = greeting;	// POINTER const
const char* const p4 = greeting;	// both const

TRICK
Imagine line down the “*”
If const to left then DATA is const
If const to right then POINTER is const

const LHS = DATA (D)
const RHS = POINTER (P)

EXAMPLE

Data	p2[0] = 'X';

error C3892: ‘p2’ : you cannot assign to a variable that is const

Pointer p3 = stevepro;			

error C3892: ‘p3’ : you cannot assign to a variable that is const

If const on left of asterisk then what’s pointed to is const i.e. data
If const on right of asterisk then the pointer itself is const i.e. pointer

DATA
Both are the same

const char* p2 = greeting;

char const* p2 = greeting;
char const *p2 = greeting;

ITERATORS
STL iterators modelled on (C) pointers
Act like T* pointer

const iterator is like pointer const (T* const pointer)
i.e. what iterator points to (RHS of *) POINTER

Thus, the iterator is not allowed to point to anything else
But the DATA i.e. the thing iterator points to may be modified

Example #1

const std::vector<int>::iterator iter = vec.begin();
*iter = 10;
//++iter;	ERROR because iterator is const pointer thus cannot point to anything else!

const_iterator
If you want iterator to point to something else (be modified) but the DATA each iteration points to
Then use const_iterator (const T* pointer)

Example #2

std::vector<int>::const_iterator cIter = vec.begin();
//*cIter = 10;		// error C3892: 'cIter' : you cannot assign to a variable that is const
++cIter;

FUNCTION DECLARATION
const refer to:
return value
input parameters
member functions as a whole

GOOD Example

class Rational { … };

const Rational operator*(const Rational& lhs, const Rational& rhs);

Why should return type operator* be const?
Because it prevents this:

Rational a, b, c;

(a * b) = c		// wouldn't proactively do this

BUT

if (a * b = c)	...	// oops, meant to do comparison

One of the hallmarks of good user-defined types is that they avoid gratuitous incompatibilities with built-in types
Declaring operator* return value as const prevents this DO IT!

const save from annoying “I meant to type ‘==’ but accidentally typed ‘=’” mistakes

const Member Functions
const Member Functions identify which member functions may be invoked on const objects.

2x REASONS important

01.Interface easier to understand:
Which functions modify an object + which don’t

02.Make it possible to work with const objects

IMP: improve performance by pass objects by reference to const
Technique only viable if there are const member functions to manipulate const-qualified objects

IMPORTANT C++ feature:
Member functions differing only in constness can be overloaded

// operator[] for const objects.
const char& operator[](std::size_t position) const
{
	return text[position];
}
// operator[] for non-const objects.
char& operator[](std::size_t position)
{
	return text[position];
}
 tb[0] = 'x'; 
ctb[0] = 'x'; 		// error! - writing to const TextBlock

Error here has to do with return type of operator[]
calls to operator[] are fine
but attempt to assign to const char& arises because return type of const version of operator[]

IMPORTANT
non-const operator[] is a reference to a char - a char would not do!

If operator[] on non-const did return simple char then statements like this wouldn’t compile:

char operator[](std::size_t position);
// error C2106: '=' : left operand must be l-value

Why?
Because it never legal to modify the return value of a function that returns a built in type (char)

If it were legal, C++ return objects by value thus a COPY of tb.text[0] would be modified
NOT tb.text[0] itself - not the behavior you want

Member functions as const
2x NOTIONS

01.Physical constness (bitwise constness)
If, and only if, member function doesn’t modify any of the object data members (excluding static)
const member function isn’t allowed to modify any of the non-static data members of the object

Violations: just look for assignments to data members
A const member function isn’t allowed to modify any of the non-static data members

1.5. Many member functions don’t act very const but pass the bitwise test

e.g.
Member function that modifies what a pointer points to (data) doesn’t act const
If the pointer is in the object then the compiler doesn’t complain WTF

const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J';		// Access violation but sometimes may be "Jello"

Problem: create const object but can modify some of its bits in the object!

02.Logical constness
A const member function might modify some of the bits in the object on which it’s invoked
But only in ways that clients cannot detect

std::size_t length() const;

NOT bitwise const as both textLength and lengthIsValid try to be modified

// error C3490: ‘textLength’ cannot be modified because it is being accessed through a const object

It should be valid for const CTextBlock objects. Compilers disagree.
They insist on bitwise constness. What to do? MUTABLE!!

class CTextBlock3
{
public:
	CTextBlock3::CTextBlock3(char* t) : pText(t), lengthIsValid(false)
	{
	}

	std::size_t length() const;

private:
	char* pText;

	mutable std::size_t textLength;
	mutable bool lengthIsValid;
};

mutable is nice solution to bitwise-constness problem

However, you may need to do similar code in const and non-const version operator[] = code duplication
Put common code into private separate member function that both version operator[] call

But may be better to implement functionality once and use it twice:
One version calls the other (cast away constness)

Have the non-const operator[] call the const version is a safe way to avoid code duplication
even though it requires a cast

// operator[] for const objects.
const char& operator[](std::size_t position) const
{
	return text[position];
}
// operator[] for non-const objects.
char& operator[](std::size_t position)
{
	return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}

Non-const version has 2x casts
As there’s no way to specify call to the const operator we cast *this from TextBlock& to const TextBlock&
Yes, we have to a cast to const! (Otherwise non-const calls itself recursively forever)

  1. to add const to *this (call const operator[]) static_cast<const TextBlock&>(*this)[position]
  2. remove const from const operator[]'s value const_cast<char&>(…) cast away constness

Finally, you do NOT want const version to call the non-const version as non-const may change things in object
(and const promises never to change anything)

But non-const can do whatever it wants and calling const member function imposes no risk
That’s why static_cast works on *this (there’s no const-related danger)

REMEMBER

  • Declaring something const helps compilers detect usage errors.
    const can be applied to objects at any scope, to function parameters and return types, and to member functions

  • Compilers enforce bitwise constness but you should program using conceptual constness

  • When const and non-const member functions have essentially identical implementations,
    code duplication can be avoided by having the non-const version call the const version
    04 Item04
    09-Nov-2013

Item 04: Make sure that objects are initialized before they’re used

int x;

x guaranteed to be initialized (to zero)

Point p;

p’s data members are sometimes guaranteed to be initialized (to zero)
but sometimes they’re not

Reading uninitialized values yields undefined behavior

Rules that describe object initialization are complicated
Not worth memorizing

e.g.
C part of C++ and initialization incur runtime cost then not guaranteed to take place

Explains why array (C part of C++) not guaranteed to have its contents initialized
But vector (STL part of C++) is

Solution: ALWAYS initialize your objects before you use them

Examples:

int x = 0;				// manual initialization of an int
const char* text = "A C-style string"	// manual initialization of a pointer

double d;
std::cin >> d;				// "initialization" by reading from input stream

Everything else, responsibility falls on constructors
Therefore, ensure constructors initialize everything in the object

NB: do not confuse assignment with initialization

//01. Assignments
ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
	// these are all assignments.
	theName = name;
	theAddress = address;
	thePhones = phones;
	numTimesConsulted = 0;
}

ABEntry objects with values you expect but not the best approach

Rules of C++ stipulate that data members of an object are initialized BEFORE
the body of a constructor is entered

ABEntry data members are assigned
Initialization took place earlier when default constructor were called automatically
before entering body of ABEntry constructor

Not true for numTimesConsulted, a built-in type
No guarantee it was initialized prior to assignment

Better way to write ABEntry (non-default) constructor
Use member initialization list instead of assignments

//02. Initialization list.
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) :
	theName(name),
	theAddress(address),
	thePhones(phones),
	numTimesConsulted(0)
{
	// the ctor body is empty.
}

Results are the same but initialization list is more efficient.
NB: theName(name) invokes copy constructor

But a single call to a copy constructor is more efficient (sometimes MUCH more efficient)
than a call to the default constructor followed by call to copy assignment operator

Built-in types there is no difference but best to initialize EVERYTHING via member initialization

Also, you can use member initialization for default-construct data members;
just specify nothing as an initialization argument

//03. Empty default-constructed values
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) :
	theName(),
	theAddress(),
	thePhones(),
	numTimesConsulted(0)
{
}

Best advice is to do member initialization for all data members
so don’t forget any plus const and reference must be initialized; they can’t be assigned!

AND initialize in the order they’re listed in data member section
because they will be initialized in the data member order i.e.theName initialized before theAddress

LAST THING
The order of initialization of non-local static objects defined in different translation units

STATIC OBJECT
An object that exists from the time it’s constructed until the end of the program

Excludes: stack / heap based objects
Includes: global objects, namespace objects, objects declare static in classes, functions, files

Local static objects are objects declared static in functions
Non-local static objects are everything else

TRANSLATION UNIT
Source code giving rise to a single object file
A single source file plus all #include files

If non-local static object in one translation unit uses a non-local static object in
different translation unit then the object it uses could be uninitialized. Why?

The relative order of initialization of non-local static objects defined in different
translation units is undefined

FileSystem + Directory example
Initialization order is important: FileSystem tfs MUST be initialized BEFORE Directory

But there is no guarantee of this because:
The relative order of initialization of non-local static objects defined in different
translation units is undefined

Why? Because it is impossible to determine the right order of initialization

Solution
Move each non-local static object into its own function where it’s declared static
These functions return references to the objects they contain

Non-local static objects are replaced with local static objects
(Singleton design pattern)

C++ guarantees that local static objects are initialized when the object’s definition
is first encountered during a call to that function

So, if you replace direct access to non-local static objects with calls to functions
that return references to local static objects then you’re guaranteed objects will initialized

Bonus: if you never call function then you never incur const of constructing / destructing obj

class FileSystem { ... };           // as before

FileSystem& tfs()                   // this replaces the tfs object; it could be
{                                   // static in the FileSystem class
  static FileSystem fs;             // define and initialize a local static object
  return fs;                        // return a reference to it
}

class Directory { ... };            // as before

Directory::Directory( params )      // as before, except references to tfs are
{                                   // now to tfs()
  std::size_t disks = tfs().numDisks();
}

Directory& tempDir()                // this replaces the tempDir object; it
{                                   // could be static in the Directory class
  static Directory td;              // define/initialize local static object
  return td;                        // return reference to it
}

Clients of program replace tfs with tfs()
i.e. use functions returning references to objects instead of using objects themselves

IMPORTANT
These functions contain static objects and makes them problematic in multi-threaded systems
non-const static object (local or non-local) may be trouble in multi-threaded systems

Solution
Manually invoke all the reference-returning functions during the single-threaded startup
That eliminates initialization-related race conditions

To avoid using objects before they’re initialized do 3x things:

  1. manually initialize non-member objects of built-in types
  2. use member initialization lists to initialize all parts of an object
  3. design around then initialization order for non-local static objects in diff translation units

REMEMBER

  • Manually initialize objects of built-in type, because C++ only sometimes initializes them

  • In a constructor, prefer member initialization lists to assignment inside constructor body.
    List data members in initialization list in the same order they’re declared in the class

  • Avoid initialization order problems across translation units by replacing
    non-local static objects with local static objects (in functions)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值