Commit 56d1379c authored by Pranjal Raihan's avatar Pranjal Raihan Committed by Facebook GitHub Bot

DelayedInit

Summary: `folly::DelayedInit` serves a similar use case to `folly::Lazy` but is suitable for data members and is thread-safe. The summary in `DelayedInit.h` provides a good overview.

Reviewed By: andriigrynenko

Differential Revision: D25671587

fbshipit-source-id: 1933b15fdc7207a52067658188ca4d5724d9c40b
parent 5295ccef
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <initializer_list>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <folly/lang/SafeAssert.h>
#include <folly/synchronization/CallOnce.h>
namespace folly {
/**
* DelayedInit -- thread-safe delayed initialization of a value. There are two
* important differences between Lazy and DelayedInit:
* 1. DelayedInit does not store the factory function inline.
* 2. DelayedInit is thread-safe.
*
* Due to these differences, DelayedInit is suitable for data members. Lazy is
* best for local stack variables.
*
* Example Usage:
*
* struct Foo {
* Bar& bar() {
* LargeState state;
* return bar_.fetch_or_construct(
* [this, &state] { return computeBar(state); });
* }
* private:
* Bar computeBar(LargeState&);
* DelayedInit<Bar> bar_;
* };
*
* If the above example were to use Lazy instead of DelayedInit:
* - Storage for LargeState and this-pointer would need to be reserved in the
* struct which wastes memory.
* - It would require additional synchronization logic for thread-safety.
*
*
* Rationale:
*
* - The stored value is initialized at most once and never deinitialized.
* Unlike Lazy, the initialization logic must be provided by the consumer.
* This means that DelayedInit is more of a "storage" type like
* std::optional. These semantics are perfect for thread-safe, lazy
* initialization of a data member.
*
* - DelayedInit models neither MoveConstructible nor CopyConstructible. The
* rationale is the same as that of std::once_flag.
*
* - There is no need for a non-thread-safe version of DelayedInit.
* std::optional will suffice in these cases.
*/
template <typename T>
struct DelayedInit {
DelayedInit() = default;
DelayedInit(const DelayedInit&) = delete;
DelayedInit& operator=(const DelayedInit&) = delete;
/**
* Gets the pre-existing value if already initialized or creates the value
* returned by the provided factory function. If the value already exists,
* then the provided function is not called.
*/
template <typename Func>
T& try_emplace_with(Func func) const {
call_once(storage_.init, [&]() mutable {
new (std::addressof(storage_.value)) T(func());
});
return storage_.value;
}
/**
* Gets the pre-existing value if already initialized or constructs the value
* in-place by direct-initializing with the provided arguments.
*/
template <typename... Args>
T& try_emplace(Args&&... args) const {
call_once(
storage_.init,
[this](Args&&... forwardedArgs) mutable {
new (std::addressof(storage_.value))
T(std::forward<Args>(forwardedArgs)...);
},
std::forward<Args>(args)...);
return storage_.value;
}
template <
typename U,
typename... Args,
typename = std::enable_if_t<
std::is_constructible<T, std::initializer_list<U>, Args...>::value>>
T& try_emplace(std::initializer_list<U> ilist, Args&&... args) const {
return try_emplace<std::initializer_list<U>, Args...>(
std::move(ilist), std::forward<Args>(args)...);
}
bool has_value() const { return test_once(storage_.init); }
explicit operator bool() const { return has_value(); }
T& value() {
require_value();
return storage_.value;
}
const T& value() const {
require_value();
return storage_.value;
}
T& operator*() {
FOLLY_SAFE_DCHECK(has_value(), "tried to access empty DelayedInit");
return storage_.value;
}
const T& operator*() const {
FOLLY_SAFE_DCHECK(has_value(), "tried to access empty DelayedInit");
return storage_.value;
}
T* operator->() {
FOLLY_SAFE_DCHECK(has_value(), "tried to access empty DelayedInit");
return std::addressof(storage_.value);
}
const T* operator->() const {
FOLLY_SAFE_DCHECK(has_value(), "tried to access empty DelayedInit");
return std::addressof(storage_.value);
}
private:
void require_value() const {
if (!has_value()) {
throw_exception<std::logic_error>("tried to access empty DelayedInit");
}
}
struct StorageTriviallyDestructible {
union {
std::remove_const_t<T> value;
};
once_flag init;
StorageTriviallyDestructible() {}
};
struct StorageNonTriviallyDestructible {
union {
std::remove_const_t<T> value;
};
once_flag init;
StorageNonTriviallyDestructible() {}
~StorageNonTriviallyDestructible() {
if (test_once(this->init)) {
this->value.~T();
}
}
};
using Storage = std::conditional_t<
std::is_trivially_destructible<T>::value,
StorageTriviallyDestructible,
StorageNonTriviallyDestructible>;
mutable Storage storage_;
};
} // namespace folly
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <folly/synchronization/DelayedInit.h>
#include <ostream>
#include <stdexcept>
#include <thread>
#include <vector>
#include <folly/Format.h>
#include <folly/portability/GTest.h>
#include <folly/synchronization/test/Barrier.h>
namespace folly {
namespace {
struct CtorCounts {
int ctor;
int copy;
int move;
CtorCounts() : CtorCounts(0, 0, 0) {}
explicit CtorCounts(int ctor, int copy, int move)
: ctor(ctor), copy(copy), move(move) {}
void reset() { *this = CtorCounts{}; }
struct Tracker {
int value;
explicit Tracker(CtorCounts& counts, int value = 0)
: value(value), counts_(counts) {
++counts_.ctor;
}
Tracker(const Tracker& other) : value(other.value), counts_(other.counts_) {
++counts_.copy;
}
Tracker(Tracker&& other) noexcept
: value(other.value), counts_(other.counts_) {
++counts_.move;
}
Tracker& operator=(const Tracker&) = delete;
Tracker& operator=(Tracker&&) = delete;
private:
CtorCounts& counts_;
};
};
bool operator==(const CtorCounts& lhs, const CtorCounts& rhs) {
return std::tie(lhs.ctor, lhs.copy, lhs.move) ==
std::tie(rhs.ctor, rhs.copy, rhs.move);
}
std::ostream& operator<<(std::ostream& out, const CtorCounts& counts) {
return out << folly::format(
"CtorCounts({}, {}, {})", counts.ctor, counts.copy, counts.move);
}
} // namespace
TEST(DelayedInit, Simple) {
CtorCounts counts;
DelayedInit<CtorCounts::Tracker> lazy;
for (int i = 0; i < 100; ++i) {
if (i > 50) {
auto& tracker = lazy.try_emplace(counts, 12);
ASSERT_EQ(tracker.value, 12);
ASSERT_EQ(counts, CtorCounts(1, 0, 0));
ASSERT_TRUE(lazy.has_value());
} else {
ASSERT_EQ(counts, CtorCounts(0, 0, 0));
ASSERT_FALSE(lazy.has_value());
}
}
}
TEST(DelayedInit, TryEmplaceWithPreservesValueCategory) {
CtorCounts counts;
CtorCounts::Tracker tracker{counts, 12};
{
counts.reset();
DelayedInit<CtorCounts::Tracker> lazy;
auto& trackerCopied = lazy.try_emplace_with(
[&]() -> const CtorCounts::Tracker& { return tracker; });
EXPECT_EQ(counts, CtorCounts(0, 1, 0));
EXPECT_EQ(trackerCopied.value, 12);
}
{
counts.reset();
DelayedInit<CtorCounts::Tracker> lazy;
auto& trackerMoved = lazy.try_emplace_with(
[&]() -> CtorCounts::Tracker&& { return std::move(tracker); });
EXPECT_EQ(counts, CtorCounts(0, 0, 1));
EXPECT_EQ(trackerMoved.value, 12);
}
}
TEST(DelayedInit, TryEmplacePreservesValueCategory) {
CtorCounts counts;
CtorCounts::Tracker tracker{counts, 12};
{
counts.reset();
DelayedInit<CtorCounts::Tracker> lazy;
auto& trackerCopied = lazy.try_emplace(tracker);
EXPECT_EQ(counts, CtorCounts(0, 1, 0));
EXPECT_EQ(trackerCopied.value, 12);
}
{
counts.reset();
DelayedInit<CtorCounts::Tracker> lazy;
auto& trackerMoved = lazy.try_emplace(std::move(tracker));
EXPECT_EQ(counts, CtorCounts(0, 0, 1));
EXPECT_EQ(trackerMoved.value, 12);
}
}
TEST(DelayedInit, TryEmplaceWithInitializerList) {
struct Thing {
explicit Thing(std::initializer_list<int> ilist) : ilist(ilist) {}
std::initializer_list<int> ilist;
};
DelayedInit<Thing> lazy;
auto& thing = lazy.try_emplace({1, 2, 3});
EXPECT_EQ(thing.ilist.size(), 3);
DelayedInit<const Thing> constLazy;
auto& constThing = constLazy.try_emplace({1, 2, 3});
EXPECT_EQ(constThing.ilist.size(), 3);
}
TEST(DelayedInit, Value) {
DelayedInit<int> lazy;
EXPECT_THROW(lazy.value(), std::logic_error);
lazy.try_emplace_with([] { return 0; });
EXPECT_EQ(lazy.value(), 0);
}
TEST(DelayedInit, CalledOnce) {
CtorCounts counts;
DelayedInit<CtorCounts::Tracker> lazy;
auto& tracker1 =
lazy.try_emplace_with([&]() { return CtorCounts::Tracker(counts, 1); });
auto& tracker2 =
lazy.try_emplace_with([&]() { return CtorCounts::Tracker(counts, 2); });
EXPECT_EQ(counts, CtorCounts(1, 0, 0));
EXPECT_EQ(tracker1.value, 1);
EXPECT_EQ(tracker2.value, 1);
}
TEST(DelayedInit, ConvertibleConstruction) {
CtorCounts counts;
DelayedInit<CtorCounts::Tracker> lazy;
auto& tracker = lazy.try_emplace(counts);
EXPECT_EQ(tracker.value, 0);
}
TEST(DelayedInit, Destructor) {
struct Thing {
explicit Thing(bool& destroyed) : destroyed_(destroyed) {}
~Thing() noexcept { destroyed_ = true; }
private:
bool& destroyed_;
};
bool destroyed = false;
{
DelayedInit<Thing> lazy;
lazy.try_emplace(destroyed);
}
EXPECT_TRUE(destroyed);
}
TEST(DelayedInit, ExceptionProof) {
DelayedInit<int> lazy;
EXPECT_THROW(
lazy.try_emplace_with([]() -> int { throw std::exception{}; }),
std::exception);
EXPECT_FALSE(lazy.has_value());
auto& value = lazy.try_emplace(1);
EXPECT_EQ(value, 1);
ASSERT_TRUE(lazy.has_value());
EXPECT_EQ(*lazy, 1);
value = lazy.try_emplace_with([]() -> int { throw std::exception{}; });
EXPECT_EQ(value, 1);
}
TEST(DelayedInit, ConstType) {
CtorCounts counts;
DelayedInit<const CtorCounts::Tracker> lazy;
EXPECT_FALSE(lazy.has_value());
auto& tracker = lazy.try_emplace(counts, 12);
EXPECT_EQ(tracker.value, 12);
EXPECT_TRUE(lazy.has_value());
EXPECT_TRUE(
(std::is_same<decltype(tracker), const CtorCounts::Tracker&>::value));
EXPECT_TRUE((
std::is_same<decltype(lazy.value()), const CtorCounts::Tracker&>::value));
EXPECT_EQ(lazy.value().value, 12);
EXPECT_TRUE(
(std::is_same<decltype(*lazy), const CtorCounts::Tracker&>::value));
EXPECT_EQ(lazy->value, 12);
}
TEST(DelayedInit, Concurrent) {
CtorCounts counts;
DelayedInit<CtorCounts::Tracker> lazy;
constexpr int N_THREADS = 100;
folly::test::Barrier barrier{N_THREADS + 1};
std::vector<std::thread> threads;
for (int i = 0; i < N_THREADS; ++i) {
threads.emplace_back([&, value = i] {
barrier.wait();
lazy.try_emplace(counts, value);
});
}
barrier.wait();
for (auto& thread : threads) {
thread.join();
}
EXPECT_EQ(counts, CtorCounts(1, 0, 0));
EXPECT_TRUE(lazy.has_value());
}
} // namespace folly
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment