Skip to content

Instantly share code, notes, and snippets.

@jmlvanre
Last active August 29, 2015 14:18
Show Gist options
  • Save jmlvanre/218eb0aa71a6fd3caead to your computer and use it in GitHub Desktop.
Save jmlvanre/218eb0aa71a6fd3caead to your computer and use it in GitHub Desktop.
Mesos C++ Upgrade

Focus:

As the full c++11 feature set is rather extensive, we want to start the upgrade to c++11 with 3 main areas of focus:

  1. Lambdas
  2. Unrestricted Unions
  3. Move semantics

Lambdas:

  • Definition:
[capture-list] (params) mutable(optional) exception attribute -> ret { body }	
  • What rule should we use for the capture-list?

    • Default capture by reference (&), specific capture by value (name of variable)

        [&, n] () {...}
    • (CXX choice) Default capture by value (=), specific capture by reference (& name of variable)

        [=, &n] () {...}
    • No default capture, specific capture by reference or value

        [m, &n] () {...}
    • Should we mandate the return type, or only specify it if the compiler can no deduce it properly?

    // Explicit return type.
    [] () -> bool { return true; }
    // Implicit return type. (**CXX choice**) 
    [] () { return true; }
  • Should we allow auto capture of lambdas?

// Capture by type.
std::function<int(bool)> lambda = [](bool dummy) -> int { return 5; };
// Auto capture. (**CXX choice**) 
auto lambda = [](bool dummy) -> int { return 5; };
  • General style questions around lambdas:
    • How do we align them?
    • Do we wrap the braces { ... } ?
    • Spacing between capture [capture] (parameters) attributes -> return type { body }?
  • No support for generic lambdas until C++14 source: http://en.cppreference.com/w/cpp/language/lambda
  • Old Way:
void _send() {
  VLOG(2) << "Finished send";
}
void send() {
  socket.send("Hello World!").then(lambda::bind(_send));
}
  • New Way:
void send() {
  socket.send("Hello World!").then(
  []() -> void {
    VLOG(2) << "Finished send";
  });
}

Unrestricted Unions:

We can now have non-pod types in unions. This allows us to

  • Overlap storage allocation for objects that never exist simultaneously:
#include <string>

struct T {
  enum {Str, Int, Double, Float} kind;
  T() : kind(Int) {}
  union Data {
    Data() : int_(5) {}
    ~Data() {}
    std::string str;
    int int_;
    double double_;
    float float_;
  } data;
};

int main() {
  printf("size of T = [%ld] bytes\n", sizeof(T));
}

output:

size of T = [16] bytes
  • Avoid the requirement of a default constructed object if it is a member of the union:
#include <vector>

class Obj {
  Obj(int _arg) : arg(_arg) {}
  private:
  Obj() = delete;
  int arg;
};

struct T {
  T() {}
  ~T() {}
  union {
    Obj obj;
  };
};

int main() {
  // This would complain about Obj not having a default constructor.
  //std::vector<Obj> data(1);

  // This works because the union only reserves space in T, it doesn't 
  // initialize obj. 
  // Now we've allocated space for an Obj, without forcing a default constructor.
  std::vector<T> data(1);
}
  • One of the concerns around using unrestricted unions is that they are tricky to get right. Specifically the control of construction and destruction of non-pod members is pushed on to the developer. This is error prone, and can cause subtle leaks. To deal with this we propose the following:
    • Only use raw unrestricted unions in performance critical library code such as Option, Try, Future. The storage of these is not likely to change because they implement very concrete concepts.
    • For non performance critical code use boost::variant. It is a little bit slower, and takes longer to compile; however, we believe this is a trade-off worth making for the safety introduced. boost::variant supports RAII and is much safer to use.

Move Semantics & Perfect Forwarding:

  • This is a wonderful feature that C developers have implemented manually for many years. At its core this is about reducing expensive copies, at 30K feet this is about making ownership and lifetime expectancy relationships more clear.
  • The STL incorporates this heavily:
std::string long_string("ABCDE...");
std::vector<std::string> data;

// This makes a copy of long_string and 
// inserts it into the vector.
data.push_back(long_string);

// This moves the temporary string into 
// the vector, swapping the data pointer, 
// and size of the string, rather than 
// copying the whole string.
data.push_back(std::string("ABCDE..."));
// Roughly Equivalent to:
{
  data.push_back(std::string());
  std::swap(long_string, data.back());
}

// This perfectly forwards the construction 
// arguments. This means we don't even swap 
// any pointers. The char pointer gets passed 
// to the constructor of the string that has 
// already been allocated inside the vector's 
// data segment.
data.emplace_back("ABCDE...");
  • For the class:
class T {
  private:
    std::vector<std::string> data;
};
  • We can define a move constructor:
  T(T&& other) {
    std::swap(data, other.data);
  }
  • We can define a move assignment operator:
  T& operator=(T&& other) {
    if (this != &other) {
      std::swap(data, other.data);
    }
    return *this;
  }
  • We can define a perfect forward constructor:
  template <typename... Args>
  T(Args&&... args) data(std::forward<Args>(args)...) {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment