Style Elements of Modern C+++-Style.pdf · Winter is coming... Unfamiliar things are hard to use...

Post on 16-Oct-2020

0 views 0 download

Transcript of Style Elements of Modern C+++-Style.pdf · Winter is coming... Unfamiliar things are hard to use...

Elements of Modern C++ Style

Adapting to the New World Order

Winter is coming...

Warning: this talk assumes working knowledge of C++98/03

Winter is coming...

“C++11 feels like a new language. I write code differently now than I did in C++98. The C++11 code is shorter, simpler, and usually more efficient than what I used to write.”

Bjarne StroustrupInventor of C++

Winter is coming...

C++ is hard to use● operator overloading● template programming● multiple inheritance● pointers● STL

Winter is coming...

Unfamiliar things are hard to use● lambdas● type deduction● move semantics● smart pointers● memory model

Winter is coming...Not your grandfather’s C++

Client MantisServer::CreateClient(const std::string &clientid, const std::string &streamid, const std::string &connectionid) {

std::shared_ptr<Client> client;

if (requestqueue_) {

requestqueue_->push({[=, &client](mantis::core::Server *server) {

client = server->CreateClient(clientid, streamid, connectionid);

}});

}

Winter is coming...Mission: completely understand what this is doing

Client MantisServer::CreateClient(const std::string &clientid, const std::string &streamid, const std::string &connectionid) {

std::shared_ptr<Client> client;

if (requestqueue_) {

requestqueue_->push({[=, &client](mantis::core::Server *server) {

client = server->CreateClient(clientid, streamid, connectionid);

}});

}

On upcoming standards

● C++14 is a minor update on 11○ fix errata○ fix holes/overlooked issues○ make features more generic

● C++17 is a major update on 11○ new std libraries for filesystem and networking○ new concurrency constructs○ optional and any types

lambdastruct Object {

int value;

};

struct ObjectTooBigPredicate {

int max;

ObjectTooBigPredicate(int m) : max(m) {}

bool operator()(const &Object obj) { return obj.value > max; }

}

void foo(const std::vector<Object> &objs) {

std::vector<Object>::const_iterator i = std::find_if(objs.begin(), objs.end(),

ObjectTooBigPredicate(5));

// *i is TOO BIG (compared to 5)!

lambdastruct Object {

int value;

};

void foo(const std::vector<Object> &objs) {

std::vector<Object>::const_iterator i = std::find_if(objs.begin(), objs.end(),

[](const Object &obj) {

return obj.value > ??;

});

// *i is TOO BIG!

lambdastruct Object {

int value;

};

void foo(const std::vector<Object> &objs) {

const int max = 5;

std::vector<Object>::const_iterator i = std::find_if(objs.begin(), objs.end(),

[max](const Object &obj) {

return obj.value > max;

});

// *i is TOO BIG (compared to 5)!

lambdastruct Object {

int value;

};

void foo(const std::vector<Object> &objs) {

size_t count = 0;

const int max = 5;

std::vector<Object>::const_iterator i = std::find_if(objs.begin(), objs.end(),

[max, &count](const Object &obj) {

if (obj.value > max) {

++count; return true;

}

return false;

});

log(“found: “ << count); // actually find_if doesn’t work this way

lambdastruct Object {

int value;

};

void foo(const std::vector<Object> &objs) {

size_t count = 0;

const int max = 5;

std::vector<Object>::const_iterator i = std::find_if(objs.begin(), objs.end(),

[&](const Object &obj) {

if (obj.value > max) {

++count; return true;

}

return false;

});

log(“found: “ << count); // actually find_if doesn’t work this way

lambdastruct Object {

int value;

};

void foo(const std::vector<Object> &objs) {

size_t count = 0;

const int max = 5;

std::function<bool(const Object &)> pred = [&](const Object &obj) {

if (obj.value > max) {

++count; return true;

}

return false;

}

std::vector<Object>::const_iterator i = std::find_if(objs.begin(), objs.end(), pred);

Element 1: Prefer lambda to bind

● std::bind is opaque to compiler optimizations● requires awkward placeholder notation● doesn’t do anything lambda can’t

Element 1: Prefer lambda to bind // turn a class method into a void function!

struct Foo {

void bar(int v) { log(“bar: ” + v); }

};

template<typename Function>

void baz(Function function) {

function();

}

//...

Foo foo;

baz([]{ foo.bar(5); }); // OK!

type deductionThe compiler already knows these types

bool foo(const std::vector<Object> &objs) {

typename std::vector<Object>::const_iterator i = objs.begin();

typename std::vector<Object>::const_iterator e = objs.end();

for (; i != e; ++i)

if (i->bar() == 5)

return false;

return true;

}

type deductionThe compiler already knows these types

auto foo(const std::vector<Object> &objs) {

auto i = begin(objs);

auto e = end(objs);

for (; i != e; ++i)

if (i->bar() == 5)

return false;

return true;

}

type deductionOr better yet

auto foo(const std::vector<Object> &objs) {

for (auto obj : objs)

if (obj.bar() == 5)

return false;

return true;

}

type deductionWorks well with STL and lambdas

auto predicate = [](const Object &obj) {

return obj.value > 5;

};

std::vector<Object> list = { a, b, c, d };

auto found = std::find_if(begin(list), end(list), pred);

if (found != end(list)) {

log(“found it!”);

}

Element 2: Prefer type deductionint foo();

//...

int main() {

size_t n = foo(); // DANGER

auto m = foo(); // OK

if (m) log(“it worked!”);

}

Element 2: Prefer type deductionObject foo();

//...

int main() {

size_t n = foo(); // ERROR

auto m = foo(); // OK

if (m) log(“it worked!”); // OK? conversion operator

}

Element 2: Prefer type deductionLooks good right?

void foo(const std::vector<Object> &objs) {

for (Object obj : objs) {

bar(obj);

}

}

Element 2: Prefer type deductionWhat if something changes?

void foo(const std::vector<Object *> &objs) {

for (Object obj : objs) { // ERROR

bar(obj);

}

}

Element 2: Prefer type deduction

void foo(const std::vector<Object *> &objs) {

for (auto obj : objs) { // LIFE IS SWEET

bar(obj); // depending on bar()

}

}

Element 2: Prefer type deduction

void foo(const std::vector<Object> &objs) {

for (const auto &obj : objs) {

bar(obj);

}

}

value semantics“There is a couple of things that make C++ unique among other contemporary mainstream programming languages. One of these things is value semantics. Value semantics is the programming style, or the way of thinking, where we focus on values that are stored in the objects rather than objects themselves. The objects are only used to convey values: we do not care about object’s identity.”

Andrzej Krzemieński

value semantics

● contrast with reference semantics○ needed with objects that have identity (actors)○ commonly found in object-oriented languages

● values are immutable and copied on assignment○ easy to reason about○ commonly found in functional languages

value semanticseasy to reason about

int foo(int v) {

v += 6;

return v;

}

int a = 5;

int b = foo(a);

log(“a: “ << a << “ b: “ << b);

value semanticseasy to reason about

int foo(int v) {

v += 6;

return v;

}

int a = 5;

int b = std::async(foo(a)).get();

log(“a: “ << a << “ b: “ << b);

value semanticseasy to reason about

std::string foo(std::string v) {

v += “6”;

return v;

}

std::string a = “5”;

std::string b = foo(a);

// a == “5”, b == “56”

value semanticseasy to reason about?

Object foo(Object obj) {

obj += 6;

return obj;

}

Object a = 5;

Object b = foo(a);

// a and b are independent values

value semanticscopy constructor/assignment

Object::Object(const Object &copy) {

// copy over internal state.

}

Object &operator=(const Object &copy) {

// copy over internal state.

}

value semanticseasy to reason about?

Object foo(Object &obj); // obj *is* a

Object a = 5;

Object b = foo(a);

// foo has side-effects on a

value semanticseasy to reason about??

Object *foo(Object *obj); // obj *is* a

Object *a = new Object(5);

Object *b = foo(a); // who is b??

// who manages these lifetimes?

value semantics● heap-based objects

○ have identity (memory address)○ cheap to move (pointer assignment)○ mutable by anyone who knows the address

● stack or register based objects○ no identity (stacks/registers go away)○ expensive to move (copy value)○ mutable only locally

value semanticsHeap allocated data + Stack allocated owner● std::vector, std::string, etc.● std::shared_ptr● copy-on-write data structures

C++03 Element: Prefer value semantics

Use stack-based objects to manage heap-based resources

move semanticsHeap allocated data + Stack allocated owner● std::vector, std::string, etc.● std::shared_ptr● copy-on-write data structures

What about std::unqiue_ptr, or std::thread??

move semanticswhat does this even mean?

Object obj; std::thread th1(foo(obj));

std::thread th2 = th1;

th2.join();

// have we spawned two threads?

// if so where is obj?

move semanticswhat does this even mean?

std::unique_ptr<Object> ptr1(new Object());

std::unique_ptr<Object> ptr2(ptr1);

// who deletes Object?

move semanticscan we make transfer of ownership explicit?

std::unique_ptr<Object> ptr1(new Object());

std::unique_ptr<Object> ptr2(std::move(ptr1));

// ptr2 deletes Object.

move semantics

1. enforces unique ownership2. avoids unnecessary copies

move semanticsmove constructor/assignment

Object::Object(Object &&move) {

// move over internal state.

}

Object &operator=(Object &&move) {

// move over internal state.

}

move semantics

std::move is a function that casts T to T&&

move semanticsavoid unnecessary copies

Object foo() {

Object bar; // default constructor

return bar;

}

Object obj(foo()); // copy in C++03

move semanticsavoid unnecessary copies

Object foo() {

Object bar; // default constructor

return bar;

}

Object obj(foo()); // move in C++11

move semanticsproblem: lots of new overloads!

Object::setBar(const Bar &bar); // copy read-only Bar

Object::setBar(Bar &&bar); // move temporary Bar

move semanticssolution: new parameter passing idiom

Object::setBar(Bar bar) { // copied or moved

mybar_ = std::move(bar); // no copy

}

Element 3: Understand move semantics

● Libraries will do most of the work for you● Libraries will expose move-only objects● Unique-owner implies move-only semantics● Move operators can save you copies● Understand parameter passing idiom

Element 3: Understand move semantics

Future APIs will look like

struct Foo {

void consume(Resource resource);

Resource produce();

void observe(const Object &object);

void share(std::shared_ptr<Object> actor);

void sink(std::unique_ptr<Object> slave);

};

smart pointers“Always use the standard smart pointers, and non-owning raw pointers. Never use owning raw pointers and delete. Use shared_ptr to express shared ownership, and use unique_ptr to express unique ownership. Prefer std::make_shared or std::make_unique to construct shared or uniquely-owned objects.”

[Paraphrased]Herb Sutter

Scott Meyers

smart pointers

● shared_ptr○ weak_ptr prevents cycles○ reference counted○ custom deleters○ thread-safe

● unique_ptr○ ownership semantics○ source/sink semantics○ automatic deletion

smart pointersavoid direct use of new/delete operators

auto sh1 = std::make_shared<Object>(a,b,c,d);

std::shared_ptr<Object> sh2 = sh1;

std::thread th(foo(sh2)); th.detach();

sh1.reset(); // sh1 no longer shares object

assert(sh1 == false);

sh2.reset(); // sh2 no longer shares object

// object will delete when thread completes

smart pointersweak_ptr

auto shared = std::make_shared<Object>(a,b,c,d);

std::weak_ptr<Object> weak = shared;

//...

auto upgraded = weak.lock();

if (!upgraded) log(“shared data expired!”);

smart pointersraw pointers can still be used to alias buffers

uint8_t buf[N];

auto p1 = &buf[i];

auto p2 = &buf[j];

swap(*p1, *p2);

memory modelSorry, this is a whole other talk!

conclusionCan anyone tell me what this does now?

Client MantisServer::CreateClient(const std::string &clientid, const std::string &streamid, const std::string &connectionid) {

std::shared_ptr<Client> client;

if (requestqueue_) {

requestqueue_->push({[=, &client](mantis::core::Server *server) {

client = server->CreateClient(clientid, streamid, connectionid);

}});

}