A Tour of C++

21 篇文章 0 订阅

A Tour of C++

Bjarne Stroustrup


Chapter 1. The Basics

The semantics of argument passing are identical to the semantics of copy initialization (type checked and implicit argument type conversion)

initialization: universal form based on curly-brace-delimited initializer lists:

complex<double> z {d1, d2};
complex<double> z3 = {1, 2}; // the = is optional and redundant

the problems caused by implicit narrowing conversions is a price paid for C compatibility (if in doubt, use the general {}-list form)

a variable should only be left uninitialized in extremely rare circumstances. don't introduce a name until you have a suitable value for it.

With auto, we use the = because there is no potentially troublesome type conversion involved (type is deduced from the initializer)

use auto except when we want to make the type clearly visible, or want to be explicit about variables' range or precision (e.g. double rather than float)


scope: local, class, namespace

constexpr: meaning roughly "to be evaluated at compile time". For a function to be usable in constexpr, define this like this: constexpr double square(double x) { return x * x; }


use overloading when functions perform conceptually the same task on different types

if a function may have to be evaluated at compile time, declare it constexpr

declare one name only per declaration

prefer {}-initializer syntax for declaration with a named type

perfer = syntax for initialization in declarations using auto

avoid unintialized variables

don't declare a variable until you have a value to initialize it with

don't say in comments what can be clearly stated in code




Chapter 2. User-Defined Types

a union is a struct in which all members are allocated at the same address so that the union occupies only as much space as its largest member

to avoid errors, one can encapsulate a union so that the correspondence between a type field and access to the union members is guaranteed. At the application level, abstractions relying on such tagged unions are common and useful, but use of "naked" unions is best minimized.

Avoid "naked" unions; wrap them in a class together with a type field


enum class is strongly typed and its enumerators are scoped

enum class Color { read, blue, green };
enum class Traffic_light { green, yellow, red };
Color col = Traffic_light::red; // error: that red is not a Color
int i = Color::red; // error

Traffic_light& operator++(Traffic_light &t) {
    switch(t) {
    case Traffic_light::green: return t = Traffic_light::yellow;
    case ...
    case ...
    }
}
Traffic_light next = ++light;

Prefer class enums over "plain" enums to minimize surprises.




Chapter 3. Modularity

C++ represents interfaces by declarations

c++ supports separate compilation

Avoid non-inline function definitions in headers

Don't put a using-directive in a header file

namespace: some declarations belong together and their names should not clash with others


The exception handling mechanism will exit scopes and function as needed (unwind) to get back to a caller that has expressed interest in handling that kind of exception, invoking destructors along the way as needed.

#include <stdexcept>

double& Vector::operator[](int i) {
    if (i < 0 || size() <= i)
        throw out_of_range("Vector::operator[]");
    return elem[i];
}

void f(Vector &v) {
    // ...
    try {
        v[v.size()] = 7;
    } catch (out_of_range) {
        // handle range error
    }
    // ...
}

a function that should never throw an exception can be declared noexcept: void user(int sz) noexcept { ... }. If user() still throws, terminate() is called.

class invariant: statement assumed to be true for a class --> it is the job of a constructor to establish the invariant for its class

handling an exception simply means doing some minimal local cleanup and rethrowing the exception

Vector::Vector(int s) {
    if (s < 0)
        throw length_error{};
    elem = new double[s];
    sz = s;
}

void test() {
    try {
        Vector v(-27);
    }
    catch (std::length_error) {
        cout << "test failed: length error\n";
        throw; // rethrow
    }
    catch (std::bad_alloc) {
        std::terminate();
    }
}

You can define your own classes to be used as exceptions and have them carry arbitrary information from a point where an error is detected to a point where it can be handled.

static_assert(A, S) prints S as a compiler error message if A is not true. Most important uses of static_assert come when we make assertions about types used as parameters in generic programming. For runtime-checked assertions, use exceptions.

What can be checked at compile time is usually best checked at compile time (using static_assert)


Throw an exception to indicate that you cannot perform an assigned task

Use exceptions for error handling

Develop an error-handling strategy early in a design

Use purpose-designed user-defined types as exceptions (not built-in types)

Don’t try to catch every exception in every function

If your function may not throw, declare it noexcept

Let a constructor establish an invariant, and throw if it cannot

Design your error-handling strategy around invariants




Chapter 12. Numerics

<cmath> errors are reported by setting errno from <cerrno> to EDOM for a domain error and to ERANGE for a range error.

void f() {
    errno = 0; // clear old error state
    sqrt(−1);
    if (errno==EDOM) cerr << "sqrt() not defined for negative argument";
    errno = 0; // clear old error state
    pow(numeric_limits<double>::max(),2);
    if (errno == ERANGE) cerr << "result of pow() too large to represent as a double";
}

Consider accumulate() , inner_product() , par tial_sum() , and adjacent_difference() before you write a loop to compute a value from a sequence

iota(b,e ,v): For each element in [ b : e ) assign ++v


Bind an engine to a distribution to get a random number generator

Be careful that your random numbers are sufficiently random

using my_engine = default_random_engine; // type of engine
using my_distribution = uniform_int_distribution<>; // type of distribution
my_engine re {}; // the default engine
my_distribution one_to_six {1,6}; // distribution that maps to the ints 1..6
auto die = bind(one_to_six,re); // make a generator
int x = die(); // roll the die: x becomes a value in [1:6]
class Rand_int {
public:
    Rand_int(int low, int high) :dist{low,high} { }
    int operator()() { return dist(re); } // draw an int
private:
    default_random_engine re;
    uniform_int_distribution<> dist;
};


the standard library provides (in <valarray> ) a vector -like template, called valarray , that is less general and more amenable to optimization for numerical computation

void f(valarray<double>& a1, valarray<double>& a2)
{
    valarray<double> a = a1∗3.14+a2/a1; // numer ic array operators *, +, /, and =
    a2 += a1∗3.14;
    a = abs(a);
    double d = a2[7];
    // ...
}

In <limits> , the standard library provides classes that describe the properties of built-in types
static_assert(numeric_limits<char>::is_signed,"unsigned characters!");
static_assert(100000<numeric_limits<int>::max(),"small ints!");




Chapter 13. Concurrency

We call a computation that can potentially be executed concurrently with other computations a task. A thread is the system-level representation of a task in a program.

void f(); // function

struct F { // function object
    void operator()(); // F’s call operator
};

void user() {
    thread t1 {f}; // f() executes in separate thread
    thread t2 {F()}; // F()() executes in separate thread
    t1.join(); // wait for t1
    t2.join(); // wait for t2, wait for the thread to terminate
}

passing arguments: uses a thread variadic template constructor that can accept an arbitrary sequence of arguments. The ref() is a type function from <functional> that unfortunately is needed to tell the variadic template to treat some_vec as a reference

void f(vector<double>& v);

struct F {
    vector<double>& v;
    F(vector<double>& vv) :v{vv} { }
    void operator()();
};

int main() {
    vector<double> some_vec {1,2,3,4,5,6,7,8,9};
    vector<double> vec2 {10,11,12,13,14};
    thread t1 {f,ref(some_vec)};
    thread t2 {F{vec2}};
    t1.join();
    t2.join();
}

returning results: (but not very elegant)

void f(const vector<double>& v, double∗ res);

class F {
public:
    F(const vector<double>& vv, double∗ p) :v{vv}, res{p} { }
    void operator()();
private:
    const vector<double>& v;
    double∗ res;
};

int main() {
    vector<double> some_vec;
    vector<double> vec2;
    // ...
    double res1;
    double res2;
    thread t1 {f,cref(some_vec),&res1};
    thread t2 {F{vec2,&res2}};
    t1.join();
    t2.join();
    cout << res1 << ' ' << res2 << '\n';
}


Sharing data: The unique_lock ’s constructor acquires the mutex (through a call m.lock())

mutex m; // controlling mutex
int sh; // shared data
void f() {
    unique_lock<mutex> lck {m}; // acquire mutex
    sh += 7; // manipulate shared data
} // release mutex implicitly

this is error-prone, and equally obviously we try to make the correspondence clear through various language means. For example: class Record { public: mutex m; //... };

The standard library offers help in the form of an operation for acquiring several locks simultaneously:

void f() {
    // ...
    unique_lock<mutex> lck1 {m1,defer_lock};
    unique_lock<mutex> lck2 {m2,defer_lock};
    unique_lock<mutex> lck3 {m3,defer_lock};
    // ...
    lock(lck1,lck2,lck3); // ... manipulate shared data ...
} // implicitly release all mutexes

The destructors for the individual unique_lock s ensure that the mutex es are released when a thread leaves the scope.


<chrono>: this_thread refers to the one and only thread

using namespace std::chrono;

auto t0 = high_resolution_clock::now();
this_thread::sleep_for(milliseconds{20});
auto t1 = high_resolution_clock::now();
cout << duration_cast<nanoseconds>(t1−t0).count() << " nanoseconds passed\n";

<condition_variable>: it allows a thread to wait for some condition (often called an event) to occur as the result of work done by other thread s.

class Message { // object to be communicated
    // ...
};

queue<Message> mqueue; // the queue of messages
condition_variable mcond; // the variable communicating events
mutex mmutex; // the locking mechanism

void consumer() {
    while(true) {
        unique_lock<mutex> lck{mmutex}; // acquire mmutex
        while (mcond.wait(lck)) /* do nothing */; // release lck and wait;
                                                  // re-acquire lck upon wakeup

        auto m = mqueue.front(); // get the message
        mqueue .pop();
        lck.unlock(); // release lck
        // ... process m ...
    }
}

void producer() {
    while(true) {
        Message m;
        // ... fill the message ...
        unique_lock<mutex> lck {mmutex}; // protect operations
        mqueue .push(m);
        mcond.notify_one(); // notify
    } // release lock (at end of scope)
}


Communicating tasks: allow programmers to operate at the conceptual level of tasks (work to potentially be done concurrently) rather than directly at the lower level of threads and locks:

[1] future and promise for returning a value from a task spawned on a separate thread
[2] packaged_task to help launch tasks and connect up the mechanisms for returning a result
[3] async() for launching of a task in a manner very similar to calling a function.

These facilities are found in <future> .

future and promise: When a task wants to pass a value to another, it puts the value into a promise . Somehow, the implementation makes that value appear in the corresponding future , from which it can be read (typically by the launcher of the task).

f we have a future<X> called fx , we can get() a value of type X from it:

X v = fx.g et(); // if necessary, wait for the value to get computed

If the value isn’t there yet, our thread is blocked until it arrives. If the value couldn’t be computed, get() might throw an exception

void f(promise<X>& px) { // a task: place the result in px
    // ...
    try {
        X res;
        // ... compute a value for res ...
        px.set_value(res);
    }
    catch (...) { // oops: couldn’t compute res
        px.set_exception(current_exception());
    }
}

void g(future<X>& fx) { // a task: get the result from fx
    // ...
    try {
        X v = fx.get(); // if necessary, wait for the value to get computed
        // ... use v ...
    }
    catch (...) { // oops: someone couldn’t compute v
        // ... handle error ...
    }
}

The packaged_task type is provided to simplify setting up tasks connected with future s and promise s to be run on thread s. A packaged_task provides wrapper code to put the return value or exception from the task into a promise

double accum(double∗ beg, double∗ end, double init) { // compute the sum of [beg:end) starting with the initial value init
    return accumulate(beg,end,init);
}
double comp2(vector<double>& v) {
    using Task_type = double(double∗,double∗,double); // type of task
    packaged_task<Task_type> pt0 {accum};
    packaged_task<Task_type> pt1 {accum}; // package the task (i.e., accum)
    future<double> f0 {pt0.get_future()};
    future<double> f1 {pt1.get_future()}; // get hold of pt1’s future
    double∗ first = &v[0];
    thread t1 {move(pt0),first,first+v.size()/2,0};
    thread t2 {move(pt1),first+v.size()/2,first+v.size(),0}; // start a thread for pt1// start a thread for pt1
    // ...
    return f0.get()+f1.get(); // get the results
}


Treat a task as a function that may happen to run concurrently with other tasks.

To launch tasks to potentially run asynchronously, we can use async() :

double comp4(vector<double>& v) { // spawn many tasks if v is large enough
    if (v.siz e()<10000) // is it worth using concurrency?
        return accum(v.begin(),v.end(),0.0);
    auto v0 = &v[0];
    auto sz = v.size();
    
    auto f0 = async(accum,v0,v0+sz/4,0.0);
    auto f1 = async(accum,v0+sz/4,v0+sz/2,0.0);
    auto f2 = async(accum,v0+sz/2,v0+sz∗3/4,0.0);
    auto f3 = async(accum,v0+sz∗3/4,v0+sz,0.0);
    return f0.get()+f1.g et()+f2.g et()+f3.g et(); // collect and combine the results
}

Using async() , you don’t hav e to think about threads and locks. Instead, you think just in terms of tasks that potentially compute their results asynchronously. There is an obvious limitation: Don’t even think of using async() for tasks that share resources needing locking


Work at the highest level of abstraction that you can afford

Atomics allow for lock-free programming

Leave lock-free programming to experts

A thread is a type-safe interface to a system thread

Use join() to wait for a thread to complete

Avoid explicitly shared data whenever you can

Use unique_lock to manage mutexes

Use lock() to acquire multiple locks

Use condition_variables to manage communication among threads

Think in terms of tasks that can be executed concurrently, rather than directly in terms of
threads

Prefer packaged_task and futures over direct use of threads and mutexes

Return a result using a promise and get a result from a future

Use packaged_tasks to handle exceptions thrown by tasks and to arrange for value return;

Use a packaged_task and a future to express a request to an external service and wait for its response

Use async() to launch simple tasks




Chapter 14. History and Compatibility

C-style casts should have been deprecated in favor of named casts: static_cast, reinterpret_cast, const_cast

Widg et∗ pw = static_cast<Widget∗>(pv); // pv is a void* supposed to point to a Widget
auto dd = reintrepret_cast<Device_driver∗>(0xFF00); // 0xFF is supposed to point to a device driver
char∗ pc = const_cast<char∗>("Casts are inherently dang erous");

Explicit type conversion can be completely avoided in most high-level code, so consider every cast (however expressed) a blemish on your design.

For class hierachy navigation, prefer the checked dynamic_cast


Macro substitution is almost never necessary in C++. Use const, constexpr, enum or enum class to define manifest constants, inline to avoid function-calling overhead, templates to specify families of functions and types, and namespaces to avoid name clashes.

Don’t use malloc() . The new operator does the same job better, and instead of realloc() , try a vector. Don’t just replace malloc() and free() with ‘‘naked’’ new and delete

Avoid void∗ , unions, and casts, except deep within the implementation of some function or class.

If you must use an explicit type conversion, use an appropriate named cast (e.g., static_cast) for a more precise statement of what you are trying to do.

Minimize the use of arrays and C-style strings.


To give a C++ function C linkage (so that it can be called from a C program fragment) or to allow a C function to be called from a C++ program fragment, declare it extern "C" .

extern "C" double sqrt(double);

Only one function of a given name in a scope can have C linkage (because C doesn’t allow function overloading).


Use constructors to establish invariants

Use constructor/destructor pairs to simplify resource management (RAII)

Avoid ‘‘naked’’ new and delete

Use containers and algorithms rather than built-in arrays and ad hoc code

Prefer standard-library facilities to locally developed code

Use exceptions, rather than error codes, to report errors that cannot be handled locally

Use move semantics to avoid copying large objects

Use unique_ptr to reference objects of polymorphic type

Use shared_ptr to reference shared objects, that is, objects without a single owner that is responsible for their destruction

Use templates to maintain static type safety (eliminate casts) and avoid unnecessary use of class hierarchies

Prefer named casts, such as static_cast over C-style casts

In C++, there are no implicit conversions from int s to enumerations; use explicit type conversion where necessary

Use extern "C" when declaring C functions;

Prefer string over C-style strings (direct manipulation of zero-terminated arrays of char )

Prefer iostream s over stdio

Prefer containers (e.g., vector ) over built-in arrays




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值