diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index a6fa9a85d9..c05b78cd57 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -188,6 +188,7 @@ add_library(common STATIC
     tiny_mt.h
     tree.h
     uint128.h
+    unique_function.h
     uuid.cpp
     uuid.h
     vector_math.h
diff --git a/src/common/unique_function.h b/src/common/unique_function.h
new file mode 100644
index 0000000000..ca05590715
--- /dev/null
+++ b/src/common/unique_function.h
@@ -0,0 +1,62 @@
+// Copyright 2021 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <utility>
+
+namespace Common {
+
+/// General purpose function wrapper similar to std::function.
+/// Unlike std::function, the captured values don't have to be copyable.
+/// This class can be moved but not copied.
+template <typename ResultType, typename... Args>
+class UniqueFunction {
+    class CallableBase {
+    public:
+        virtual ~CallableBase() = default;
+        virtual ResultType operator()(Args&&...) = 0;
+    };
+
+    template <typename Functor>
+    class Callable final : public CallableBase {
+    public:
+        Callable(Functor&& functor_) : functor{std::move(functor_)} {}
+        ~Callable() override = default;
+
+        ResultType operator()(Args&&... args) override {
+            return functor(std::forward<Args>(args)...);
+        }
+
+    private:
+        Functor functor;
+    };
+
+public:
+    UniqueFunction() = default;
+
+    template <typename Functor>
+    UniqueFunction(Functor&& functor)
+        : callable{std::make_unique<Callable<Functor>>(std::move(functor))} {}
+
+    UniqueFunction& operator=(UniqueFunction&& rhs) noexcept = default;
+    UniqueFunction(UniqueFunction&& rhs) noexcept = default;
+
+    UniqueFunction& operator=(const UniqueFunction&) = delete;
+    UniqueFunction(const UniqueFunction&) = delete;
+
+    ResultType operator()(Args&&... args) const {
+        return (*callable)(std::forward<Args>(args)...);
+    }
+
+    explicit operator bool() const noexcept {
+        return static_cast<bool>(callable);
+    }
+
+private:
+    std::unique_ptr<CallableBase> callable;
+};
+
+} // namespace Common
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 96bc30cac3..c4c012f3d8 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -5,6 +5,7 @@ add_executable(tests
     common/host_memory.cpp
     common/param_package.cpp
     common/ring_buffer.cpp
+    common/unique_function.cpp
     core/core_timing.cpp
     core/network/network.cpp
     tests.cpp
diff --git a/src/tests/common/unique_function.cpp b/src/tests/common/unique_function.cpp
new file mode 100644
index 0000000000..ac9912738a
--- /dev/null
+++ b/src/tests/common/unique_function.cpp
@@ -0,0 +1,108 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <string>
+
+#include <catch2/catch.hpp>
+
+#include "common/unique_function.h"
+
+namespace {
+struct Noisy {
+    Noisy() : state{"Default constructed"} {}
+    Noisy(Noisy&& rhs) noexcept : state{"Move constructed"} {
+        rhs.state = "Moved away";
+    }
+    Noisy& operator=(Noisy&& rhs) noexcept {
+        state = "Move assigned";
+        rhs.state = "Moved away";
+    }
+    Noisy(const Noisy&) : state{"Copied constructed"} {}
+    Noisy& operator=(const Noisy&) {
+        state = "Copied assigned";
+    }
+
+    std::string state;
+};
+} // Anonymous namespace
+
+TEST_CASE("UniqueFunction", "[common]") {
+    SECTION("Capture reference") {
+        int value = 0;
+        Common::UniqueFunction<void> func = [&value] { value = 5; };
+        func();
+        REQUIRE(value == 5);
+    }
+    SECTION("Capture pointer") {
+        int value = 0;
+        int* pointer = &value;
+        Common::UniqueFunction<void> func = [pointer] { *pointer = 5; };
+        func();
+        REQUIRE(value == 5);
+    }
+    SECTION("Move object") {
+        Noisy noisy;
+        REQUIRE(noisy.state == "Default constructed");
+
+        Common::UniqueFunction<void> func = [noisy = std::move(noisy)] {
+            REQUIRE(noisy.state == "Move constructed");
+        };
+        REQUIRE(noisy.state == "Moved away");
+        func();
+    }
+    SECTION("Move construct function") {
+        int value = 0;
+        Common::UniqueFunction<void> func = [&value] { value = 5; };
+        Common::UniqueFunction<void> new_func = std::move(func);
+        new_func();
+        REQUIRE(value == 5);
+    }
+    SECTION("Move assign function") {
+        int value = 0;
+        Common::UniqueFunction<void> func = [&value] { value = 5; };
+        Common::UniqueFunction<void> new_func;
+        new_func = std::move(func);
+        new_func();
+        REQUIRE(value == 5);
+    }
+    SECTION("Default construct then assign function") {
+        int value = 0;
+        Common::UniqueFunction<void> func;
+        func = [&value] { value = 5; };
+        func();
+        REQUIRE(value == 5);
+    }
+    SECTION("Pass arguments") {
+        int result = 0;
+        Common::UniqueFunction<void, int, int> func = [&result](int a, int b) { result = a + b; };
+        func(5, 4);
+        REQUIRE(result == 9);
+    }
+    SECTION("Pass arguments and return value") {
+        Common::UniqueFunction<int, int, int> func = [](int a, int b) { return a + b; };
+        REQUIRE(func(5, 4) == 9);
+    }
+    SECTION("Destructor") {
+        int num_destroyed = 0;
+        struct Foo {
+            Foo(int* num_) : num{num_} {}
+            Foo(Foo&& rhs) : num{std::exchange(rhs.num, nullptr)} {}
+            Foo(const Foo&) = delete;
+
+            ~Foo() {
+                if (num) {
+                    ++*num;
+                }
+            }
+
+            int* num = nullptr;
+        };
+        Foo object{&num_destroyed};
+        {
+            Common::UniqueFunction<void> func = [object = std::move(object)] {};
+            REQUIRE(num_destroyed == 0);
+        }
+        REQUIRE(num_destroyed == 1);
+    }
+}