Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cbded5e3 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Introduce ftl::Finalizer{<F>,Std,Ftl,Ftl1,Ftl2,Ftl3}" into main

parents 5ce9acdf b7ce156d
Loading
Loading
Loading
Loading
+211 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 The Android Open Source Project
 *
 * 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 <cstddef>

#include <functional>
#include <type_traits>
#include <utility>

#include <ftl/function.h>

namespace android::ftl {

// An RAII wrapper that invokes a function object as a finalizer when destroyed.
//
// The function object must take no arguments, and must return void. If the function object needs
// any context for the call, it must store it itself, for example with a lambda capture.
//
// The stored function object will be called once (unless canceled via the `cancel()` member
// function) at the first of:
//
//   - The Finalizer instance is destroyed.
//   - `operator()` is used to invoke the contained function.
//   - The Finalizer instance is move-assigned a new value. The function being replaced will be
//     invoked, and the replacement will be stored to be called later.
//
// The intent with this class is to keep cleanup code next to the code that requires that
// cleanup be performed.
//
//   bool read_file(std::string filename) {
//     FILE* f = fopen(filename.c_str(), "rb");
//     if (f == nullptr) return false;
//     const auto cleanup = ftl::Finalizer([f]() { fclose(f); });
//     // fread(...), etc
//     return true;
//   }
//
// The `FinalFunction` template argument to Finalizer<FinalFunction> allows a polymorphic function
// type for storing the finalization function, such as `std::function` or `ftl::Function`.
//
// For convenience, this header defines a few useful aliases for using those types.
//
//   - `FinalizerStd`, an alias for `Finalizer<std::function<void()>>`
//   - `FinalizerFtl`, an alias for `Finalizer<ftl::Function<void()>>`
//   - `FinalizerFtl1`, an alias for `Finalizer<ftl::Function<void(), 1>>`
//   - `FinalizerFtl2`, an alias for `Finalizer<ftl::Function<void(), 2>>`
//   - `FinalizerFtl3`, an alias for `Finalizer<ftl::Function<void(), 3>>`
//
// Clients of this header are free to define other aliases they need.
//
// A Finalizer that uses a polymorphic function type can be returned from a function call and/or
// stored as member data (to be destroyed along with the containing class).
//
//   auto register(Observer* observer) -> ftl::FinalizerStd<void()> {
//      const auto id = observers.add(observer);
//      return ftl::Finalizer([id]() { observers.remove(id); });
//   }
//
//   {
//     const auto _ = register(observer);
//     // do the things that required the registered observer.
//   }
//   // the observer is removed.
//
// Cautions:
//
//   1. When a Finalizer is stored as member data, you will almost certainly want that cleanup to
//      happen first, before the rest of the other member data is destroyed. For safety you should
//      assume that the finalization function will access that data directly or indirectly.
//
//      This means that Finalizers should be defined last, after all other normal member data in a
//      class.
//
//          class MyClass {
//           public:
//            bool initialize() {
//              ready_ = true;
//              cleanup_ = ftl::Finalizer([this]() { ready_ = false; });
//              return true;
//            }
//
//            bool ready_ = false;
//
//            // Finalizers should be last so other class members can be accessed before being
//            // destroyed.
//            ftl::FinalizerStd<void()> cleanup_;
//          };
//
//   2. Care must be taken to use `ftl::Finalizer()` when constructing locally from a lambda. If you
//      forget to do so, you are just creating a lambda that won't be automatically invoked!
//
//          const auto bad = [&counter](){ ++counter; }; // Just a lambda instance
//          const auto good = ftl::Finalizer([&counter](){ ++counter; });
//
template <typename FinalFunction>
class Finalizer final {
  // requires(std::is_invocable_r_v<void, FinalFunction>)
  static_assert(std::is_invocable_r_v<void, FinalFunction>);

 public:
  // A default constructed Finalizer does nothing when destroyed.
  // requires(std::is_default_constructible_v<FinalFunction>)
  constexpr Finalizer() = default;

  // Constructs a Finalizer from a function object.
  // requires(std::is_invocable_v<F>)
  template <typename F, typename = std::enable_if_t<std::is_invocable_v<F>>>
  [[nodiscard]] explicit constexpr Finalizer(F&& function)
      : Finalizer(std::forward<F>(function), false) {}

  constexpr ~Finalizer() { maybe_invoke(); }

  // Disallow copying.
  Finalizer(const Finalizer& that) = delete;
  auto operator=(const Finalizer& that) = delete;

  // Move construction
  // requires(std::is_move_constructible_v<FinalFunction>)
  [[nodiscard]] constexpr Finalizer(Finalizer&& that)
      : Finalizer(std::move(that.function_), std::exchange(that.canceled_, true)) {}

  // Implicit conversion move construction
  // requires(!std::is_same_v<Finalizer, Finalizer<F>>)
  template <typename F, typename = std::enable_if_t<!std::is_same_v<Finalizer, Finalizer<F>>>>
  // NOLINTNEXTLINE(google-explicit-constructor, cppcoreguidelines-rvalue-reference-param-not-moved)
  [[nodiscard]] constexpr Finalizer(Finalizer<F>&& that)
      : Finalizer(std::move(that.function_), std::exchange(that.canceled_, true)) {}

  // Move assignment
  // requires(std::is_move_assignable_v<FinalFunction>)
  constexpr auto operator=(Finalizer&& that) -> Finalizer& {
    maybe_invoke();

    function_ = std::move(that.function_);
    canceled_ = std::exchange(that.canceled_, true);

    return *this;
  }

  // Implicit conversion move assignment
  // requires(!std::is_same_v<Finalizer, Finalizer<F>>)
  template <typename F, typename = std::enable_if_t<!std::is_same_v<Finalizer, Finalizer<F>>>>
  // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
  constexpr auto operator=(Finalizer<F>&& that) -> Finalizer& {
    *this = Finalizer(std::move(that.function_), std::exchange(that.canceled_, true));
    return *this;
  }

  // Cancels the final function, preventing it from being invoked.
  constexpr void cancel() {
    canceled_ = true;
    maybe_nullify_function();
  }

  // Invokes the final function now, if not already invoked.
  constexpr void operator()() { maybe_invoke(); }

 private:
  template <typename>
  friend class Finalizer;

  template <typename F, typename = std::enable_if_t<std::is_invocable_v<F>>>
  [[nodiscard]] explicit constexpr Finalizer(F&& function, bool canceled)
      : function_(std::forward<F>(function)), canceled_(canceled) {}

  constexpr void maybe_invoke() {
    if (!std::exchange(canceled_, true)) {
      std::invoke(function_);
      maybe_nullify_function();
    }
  }

  constexpr void maybe_nullify_function() {
    // Sets function_ to nullptr if that is supported for the backing type.
    if constexpr (std::is_assignable_v<FinalFunction, nullptr_t>) {
      function_ = nullptr;
    }
  }

  FinalFunction function_;
  bool canceled_ = true;
};

template <typename F>
Finalizer(F&&) -> Finalizer<std::decay_t<F>>;

// A standard alias for using `std::function` as the polymorphic function type.
using FinalizerStd = Finalizer<std::function<void()>>;

// Helpful aliases for using `ftl::Function` as the polymorphic function type.
using FinalizerFtl = Finalizer<Function<void()>>;
using FinalizerFtl1 = Finalizer<Function<void(), 1>>;
using FinalizerFtl2 = Finalizer<Function<void(), 2>>;
using FinalizerFtl3 = Finalizer<Function<void(), 3>>;

}  // namespace android::ftl
 No newline at end of file
+1 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ cc_test {
        "enum_test.cpp",
        "expected_test.cpp",
        "fake_guard_test.cpp",
        "finalizer_test.cpp",
        "flags_test.cpp",
        "function_test.cpp",
        "future_test.cpp",
+209 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 The Android Open Source Project
 *
 * 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 <memory>
#include <type_traits>
#include <utility>

#include <ftl/finalizer.h>
#include <gtest/gtest.h>

namespace android::test {

namespace {

struct Counter {
  constexpr auto increment_fn() {
    return [this] { ++value_; };
  }

  auto increment_finalizer() {
    return ftl::Finalizer([this] { ++value_; });
  }

  [[nodiscard]] constexpr auto value() const -> int { return value_; }

 private:
  int value_ = 0;
};

struct CounterPair {
  constexpr auto increment_first_fn() { return first.increment_fn(); }
  constexpr auto increment_second_fn() { return second.increment_fn(); }
  [[nodiscard]] constexpr auto values() const -> std::pair<int, int> {
    return {first.value(), second.value()};
  }

 private:
  Counter first;
  Counter second;
};

}  // namespace

TEST(Finalizer, DefaultConstructionAndNoOpDestructionWhenPolymorphicType) {
  ftl::FinalizerStd finalizer1;
  ftl::FinalizerFtl finalizer2;
  ftl::FinalizerFtl1 finalizer3;
  ftl::FinalizerFtl2 finalizer4;
  ftl::FinalizerFtl3 finalizer5;
}

TEST(Finalizer, InvokesTheFunctionOnDestruction) {
  Counter counter;
  {
    const auto finalizer = counter.increment_finalizer();
    EXPECT_EQ(counter.value(), 0);
  }
  EXPECT_EQ(counter.value(), 1);
}

TEST(Finalizer, InvocationCanBeCanceled) {
  Counter counter;
  {
    auto finalizer = counter.increment_finalizer();
    EXPECT_EQ(counter.value(), 0);
    finalizer.cancel();
    EXPECT_EQ(counter.value(), 0);
  }
  EXPECT_EQ(counter.value(), 0);
}

TEST(Finalizer, InvokesTheFunctionOnce) {
  Counter counter;
  {
    auto finalizer = counter.increment_finalizer();
    EXPECT_EQ(counter.value(), 0);
    finalizer();
    EXPECT_EQ(counter.value(), 1);
    finalizer();
    EXPECT_EQ(counter.value(), 1);
  }
  EXPECT_EQ(counter.value(), 1);
}

TEST(Finalizer, SelfInvocationIsAllowedAndANoOp) {
  Counter counter;
  ftl::FinalizerStd finalizer;
  finalizer = ftl::Finalizer([&]() {
    counter.increment_fn()();
    finalizer();  // recursive invocation should do nothing.
  });
  EXPECT_EQ(counter.value(), 0);
  finalizer();
  EXPECT_EQ(counter.value(), 1);
}

TEST(Finalizer, MoveConstruction) {
  Counter counter;
  {
    ftl::FinalizerStd outer_finalizer = counter.increment_finalizer();
    EXPECT_EQ(counter.value(), 0);
    {
      ftl::FinalizerStd inner_finalizer = std::move(outer_finalizer);
      static_assert(std::is_same_v<decltype(inner_finalizer), decltype(outer_finalizer)>);
      EXPECT_EQ(counter.value(), 0);
    }
    EXPECT_EQ(counter.value(), 1);
  }
  EXPECT_EQ(counter.value(), 1);
}

TEST(Finalizer, MoveConstructionWithImplicitConversion) {
  Counter counter;
  {
    auto outer_finalizer = counter.increment_finalizer();
    EXPECT_EQ(counter.value(), 0);
    {
      ftl::FinalizerStd inner_finalizer = std::move(outer_finalizer);
      static_assert(!std::is_same_v<decltype(inner_finalizer), decltype(outer_finalizer)>);
      EXPECT_EQ(counter.value(), 0);
    }
    EXPECT_EQ(counter.value(), 1);
  }
  EXPECT_EQ(counter.value(), 1);
}

TEST(Finalizer, MoveAssignment) {
  CounterPair pair;
  {
    ftl::FinalizerStd outer_finalizer = ftl::Finalizer(pair.increment_first_fn());
    EXPECT_EQ(pair.values(), std::make_pair(0, 0));

    {
      ftl::FinalizerStd inner_finalizer = ftl::Finalizer(pair.increment_second_fn());
      static_assert(std::is_same_v<decltype(inner_finalizer), decltype(outer_finalizer)>);
      EXPECT_EQ(pair.values(), std::make_pair(0, 0));
      inner_finalizer = std::move(outer_finalizer);
      EXPECT_EQ(pair.values(), std::make_pair(0, 1));
    }
    EXPECT_EQ(pair.values(), std::make_pair(1, 1));
  }
  EXPECT_EQ(pair.values(), std::make_pair(1, 1));
}

TEST(Finalizer, MoveAssignmentWithImplicitConversion) {
  CounterPair pair;
  {
    auto outer_finalizer = ftl::Finalizer(pair.increment_first_fn());
    EXPECT_EQ(pair.values(), std::make_pair(0, 0));

    {
      ftl::FinalizerStd inner_finalizer = ftl::Finalizer(pair.increment_second_fn());
      static_assert(!std::is_same_v<decltype(inner_finalizer), decltype(outer_finalizer)>);
      EXPECT_EQ(pair.values(), std::make_pair(0, 0));
      inner_finalizer = std::move(outer_finalizer);
      EXPECT_EQ(pair.values(), std::make_pair(0, 1));
    }
    EXPECT_EQ(pair.values(), std::make_pair(1, 1));
  }
  EXPECT_EQ(pair.values(), std::make_pair(1, 1));
}

TEST(Finalizer, NullifiesTheFunctionWhenInvokedIfPossible) {
  auto shared = std::make_shared<int>(0);
  std::weak_ptr<int> weak = shared;

  int count = 0;
  {
    auto lambda = [capture = std::move(shared)]() {};
    auto finalizer = ftl::Finalizer(std::move(lambda));
    EXPECT_FALSE(weak.expired());

    // A lambda is not nullable. Invoking the finalizer cannot destroy it to destroy the lambda's
    // capture.
    finalizer();
    EXPECT_FALSE(weak.expired());
  }
  // The lambda is only destroyed when the finalizer instance is destroyed.
  EXPECT_TRUE(weak.expired());

  shared = std::make_shared<int>(0);
  weak = shared;

  {
    auto lambda = [capture = std::move(shared)]() {};
    auto finalizer = ftl::FinalizerStd(std::move(lambda));
    EXPECT_FALSE(weak.expired());

    // Since std::function is used, and is nullable, invoking the finalizer will destroy the
    // contained function, which will destroy the lambda's capture.
    finalizer();
    EXPECT_TRUE(weak.expired());
  }
}

}  // namespace android::test