Copy-on-write added to POC

Reviewers: buda, mislav.bradac

Reviewed By: mislav.bradac

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D848
This commit is contained in:
florijan 2017-10-03 09:29:07 +02:00
parent 0c186fe76f
commit 3ca9acb90a
2 changed files with 173 additions and 0 deletions

106
poc/utils/copy_on_write.hpp Normal file
View File

@ -0,0 +1,106 @@
#pragma once
#include <memory>
#include "utils/assert.hpp"
namespace utils {
/**
* Helper class for implementing copy-on-write member variables. Memory
* management is automatic via shared-ptr: it is allowed for the original
* owner to expire, the content will remain valid in copies.
*
* This class is generally not thread-safe. However, it is intended for use in
* Memgraph's MVCC which is concurrent, but with specific guarantees that also
* make it possible to use this. These guarantees are:
* 1. Writing can only happen when there are no copies (no parallel reads). An
* implication of this is that an obtained copy is immutable, even though in
* general CopyOnWrite does not guarantee this (generally the owner's
* modificatins will be visible to the copies).
* 2. Copies are not created in parallel. MVCC guarantees this with
* record-locking, but should be in fact legal in CopyOnWrite.
*
* @tparam TElement - type of content. Must be copy-constructable.
*/
template <typename TElement>
class CopyOnWrite {
public:
/**
* Creates a new CopyOnWrite that owns it's element.
*
* @param args - Arguments forwarded to the TElement constructor.
* @tparam TArgs - Argument types.
*/
template <typename... TArgs>
CopyOnWrite(TArgs &&... args) {
TElement *new_element = new TElement(std::forward<TArgs>(args)...);
element_ptr_.reset(new_element);
is_owner_ = true;
}
/** Creates a copy of the given CopyOnWrite object that does not copy the
* element and does not assume ownership over it. */
CopyOnWrite(const CopyOnWrite &other)
: element_ptr_{other.element_ptr_}, is_owner_{false} {}
/** Creates a copy of the given CopyOnWrite object that does not copy the
* element and does not assume ownership over it. This is a non-const
* reference accepting copy constructor. The hack is necessary to prevent the
* variadic constructor from being a better match for non-const CopyOnWrite
* argument. */
CopyOnWrite(CopyOnWrite &other)
: CopyOnWrite(const_cast<const CopyOnWrite &>(other)) {}
/** Creates a copy of the given temporary CyopOnWrite object. If the temporary
* is owner then ownership is transferred to this CopyOnWrite. Otherwise this
* CopyOnWrite does not become the owner. */
CopyOnWrite(CopyOnWrite &&other) = default;
/** Copy assignment of another CopyOnWrite. Does not transfer ownership (this
* CopyOnWrite is not the owner). */
CopyOnWrite &operator=(const CopyOnWrite &other) {
element_ptr_ = other.element_ptr_;
is_owner_ = false;
return *this;
}
/** Copy assignment of a temporary CopyOnWrite. If the temporary is owner then
* ownerships is transferred to this CopyOnWrite. Otherwise this CopyOnWrite
* does not become the owner. */
CopyOnWrite &operator=(CopyOnWrite &&other) = default;
// All the dereferencing operators are overloaded to return a const element
// reference. There is no differentiation between const and non-const member
// function behavior because an implicit copy creation on non-const
// dereferencing would most likely result in excessive copying. For that
// reason
// an explicit call to the `Write` function is required to obtain a non-const
// reference to element.
const TElement &operator*() { return *element_ptr_; }
const TElement &operator*() const { return *element_ptr_; }
const TElement &get() { return *element_ptr_; }
const TElement &get() const { return *element_ptr_; }
const TElement *operator->() { return element_ptr_.get(); }
const TElement *operator->() const { return element_ptr_.get(); }
/** Indicates if this CopyOnWrite object is the owner of it's element. */
bool is_owner() const { return is_owner_; };
/**
* If this CopyOnWrite is the owner of it's element, a non-const reference to
* is returned. If this CopyOnWrite is not the owner, then the element is
* copied and this CopyOnWrite becomes the owner.
*/
TElement &Write() {
if (is_owner_) return *element_ptr_;
element_ptr_ = std::shared_ptr<TElement>(new TElement(*element_ptr_));
is_owner_ = true;
return *element_ptr_;
};
private:
std::shared_ptr<TElement> element_ptr_;
bool is_owner_{false};
};
} // namespace utils

View File

@ -0,0 +1,67 @@
#include "gtest/gtest.h"
#include "utils/copy_on_write.hpp"
TEST(CopyOneWrite, Dereferencing) {
utils::CopyOnWrite<std::string> number("42");
EXPECT_EQ((*number).size(), 2);
EXPECT_EQ(number.get().size(), 2);
EXPECT_EQ(number->size(), 2);
}
TEST(CopyOneWrite, Ownership) {
utils::CopyOnWrite<std::string> number("42");
EXPECT_TRUE(number.is_owner());
utils::CopyOnWrite<std::string> copy_constructed(number);
auto copy_assigned = number;
EXPECT_FALSE(copy_constructed.is_owner());
EXPECT_FALSE(copy_assigned.is_owner());
EXPECT_TRUE(number.is_owner());
}
TEST(CopyOneWrite, OwnershipFromTemporary) {
utils::CopyOnWrite<std::string> copy_constructed(
utils::CopyOnWrite<std::string>("42"));
auto copy_assigned = utils::CopyOnWrite<std::string>("42");
EXPECT_TRUE(copy_constructed.is_owner());
EXPECT_TRUE(copy_assigned.is_owner());
}
struct DestructorCounter {
DestructorCounter(int &counter) : counter_(counter) {}
~DestructorCounter() {
counter_++;
}
private:
int &counter_;
};
TEST(CopyOnWrite, ElementDestruction) {
int counter = 0;
std::vector<utils::CopyOnWrite<DestructorCounter>> initial_owner;
initial_owner.emplace_back(counter);
{
auto copy = initial_owner[0];
EXPECT_EQ(counter, 0);
initial_owner.clear();
EXPECT_EQ(counter, 0);
}
EXPECT_EQ(counter, 1);
}
TEST(CopyOneWrite, Copy) {
utils::CopyOnWrite<std::string> number("42");
auto copy = number;
EXPECT_EQ(*copy, "42");
// modification by owner
number.Write().resize(1);
EXPECT_EQ(*number, "4");
EXPECT_EQ(*copy, "4");
// modification by copy
copy.Write().resize(2, '3');
EXPECT_EQ(*number, "4");
EXPECT_EQ(*copy, "43");
}