if constexpr() for C++14 (#2654)

* - Use C++14 library stuff if possible

* Added a C++14 of if constexpr(). I think it's pretty great. Very similar to hana::if_

* - added some example usages

* - added is_detected
- added more tests

* - fix

* - fix

* - much better. thanks Davis

* - fix

* - fix

* - fix

* - Davis, I think you were wrong. Here, everything breaks without the _ function

* - Fixed but docs suck

* - use C++14 stuff

* - put function body after docs

* - docs maybe slightly better but still suck

* - badly placed brace

* - maybe slightly better English

* - better API i think

Co-authored-by: pf <pf@me>
This commit is contained in:
pfeatherstone 2022-09-01 13:34:32 +01:00 committed by GitHub
parent 902c70193a
commit 4db03ee6fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 493 additions and 152 deletions

138
dlib/constexpr_if.h Normal file
View File

@ -0,0 +1,138 @@
// Copyright (C) 2022 Davis E. King (davis@dlib.net)
// License: Boost Software License See LICENSE.txt for the full license.
#ifndef DLIB_IF_CONSTEXPR_H
#define DLIB_IF_CONSTEXPR_H
#include "overloaded.h"
namespace dlib
{
// ----------------------------------------------------------------------------------------
namespace detail
{
const auto _ = [](auto&& arg) -> decltype(auto) { return std::forward<decltype(arg)>(arg); };
template<typename...T>
using void_t = void;
template<typename Void, template <class...> class Op, class... Args>
struct is_detected : std::false_type{};
template<template <class...> class Op, class... Args>
struct is_detected<void_t<Op<Args...>>, Op, Args...> : std::true_type {};
}
// ----------------------------------------------------------------------------------------
template<typename... T>
struct types_ {};
/*!
WHAT THIS OBJECT REPRESENTS
This is a type list. Use this to pass types to the switch_() function as
compile-time initial conditions.
!*/
// ----------------------------------------------------------------------------------------
template<bool... v>
auto bools(std::integral_constant<bool, v>...)
/*!
ensures
- returns a type list of compile time booleans.
!*/
{
return types_<std::integral_constant<bool,v>...>{};
}
using true_t = types_<std::true_type>;
using false_t = types_<std::false_type>;
// ----------------------------------------------------------------------------------------
template <
typename... T,
typename... Cases
>
constexpr decltype(auto) switch_(
types_<T...> /*meta_obj*/,
Cases&&... cases
)
/*!
requires
- meta_obj combines a set of initial types. These are used as compile-time initial conditions.
- cases is a set of overload-able conditional branches.
- at least one of the cases is callable given meta_obj.
- each case statement has signature auto(types_<>..., auto _) where _ is an identity function
with identical behaviour to std::identity. This is used to make each generic lambda artificially
dependent on the function body. This allows semantic analysis of the lambdas to be performed AFTER
the correct lambda is chosen depending on meta_obj. This is the crucial bit that makes switch_() behave
in a similar way to "if constexpr()" in C++17. Make sure to use _ on one of the objects in the lambdas.
ensures
- calls the correct conditional branch.
- the correct conditional branch is selected at compile-time.
- Note, each branch can return different types, and the return type of the switch_() function
is that of the compile-time selected branch.
Here is an example:
template<typename T>
auto perform_correct_action(T& obj)
{
return switch(
types_<T>{},
[&](types_<A>, auto _) {
return _(obj).set_something_specific_to_A_and_return_something();
},
[&](types_<B>, auto _) {
return _(obj).set_something_specific_to_B_and_return_something();
},
[&](auto...) {
// Default case statement. Do something sensible.
return false;
}
);
}
Here is another example:
template<typename T>
auto transfer_state(T& a, T& b)
{
return switch(
bools(std::is_move_constructible<T>{}, std::is_copy_constructible<T>{}),
[&](true_t, auto, auto _) {
// T is both move-constructible. Copy semantics can be anything
a = std::move(_(b));
return move_tag{}; // Just for fun, we return different types in each branch.
},
[&](auto, true_t, auto _) {
// T is copy-constructible, Move semantics can be anything. Though in this case,
// if it had been move-constructible, the first branch would have been selected.
// So in this case, it is not move-constructible.
a = _(b);
return copy_tag{};
},
[&](auto...) {
// Default case statement
return dont_care_tag{};
}
);
}
!*/
{
return overloaded(std::forward<Cases>(cases)...)(types_<T>{}..., detail::_);
}
// ----------------------------------------------------------------------------------------
template<template <class...> class Op, class... Args>
using is_detected = detail::is_detected<void, Op, Args...>;
/*!
ensures
- This is exactly the same as std::experimental::is_detected from library fundamentals v
!*/
// ----------------------------------------------------------------------------------------
}
#endif //DLIB_IF_CONSTEXPR_H

115
dlib/overloaded.h Normal file
View File

@ -0,0 +1,115 @@
// Copyright (C) 2022 Davis E. King (davis@dlib.net)
// License: Boost Software License See LICENSE.txt for the full license.
#ifndef DLIB_OVERLOADED_H_
#define DLIB_OVERLOADED_H_
#include <utility>
#include <type_traits>
namespace dlib
{
namespace detail
{
#if __cpp_fold_expressions
template<typename ...Base>
struct overloaded_helper : Base...
{
template<typename... T>
overloaded_helper(T&& ... t) : Base{std::forward<T>(t)}... {}
using Base::operator()...;
};
#else
template<typename Base, typename ... BaseRest>
struct overloaded_helper: Base, overloaded_helper<BaseRest...>
{
template<typename T, typename ... TRest>
overloaded_helper(T&& t, TRest&& ...trest) :
Base{std::forward<T>(t)},
overloaded_helper<BaseRest...>{std::forward<TRest>(trest)...}
{}
using Base::operator();
using overloaded_helper<BaseRest...>::operator();
};
template<typename Base>
struct overloaded_helper<Base> : Base
{
template<typename T>
overloaded_helper<Base>(T&& t) : Base{std::forward<T>(t)}
{}
using Base::operator();
};
#endif //__cpp_fold_expressions
}
template<typename... T>
auto overloaded(T&&... t)
/*!
This is a helper function for combining many callable objects (usually lambdas), into
an overload-able set. This can be used in visitor patterns like
- dlib::type_safe_union::apply_to_contents()
- dlib::visit()
- dlib::for_each()
- dlib::switch_()
A picture paints a thousand words:
using tsu = type_safe_union<int,float,std::string>;
tsu a = std::string("hello there");
std::string result;
a.apply_to_contents(overloaded(
[&result](int) {
result = std::string("int");
},
[&result](float) {
result = std::string("float");
},
[&result](const std::string& item) {
result = item;
}
));
assert(result == "hello there");
result = "";
result = visit(overloaded(
[](int) {
return std::string("int");
},
[](float) {
return std::string("float");
},
[](const std::string& item) {
return item;
}
), a);
assert(result == "hello there");
std::vector<int> type_ids;
for_each_type(a, overloaded(
[&type_ids](in_place_tag<int>, tsu& me) {
type_ids.push_back(me.get_type_id<int>());
},
[&type_ids](in_place_tag<float>, tsu& me) {
type_ids.push_back(me.get_type_id<float>());
},
[&type_ids](in_place_tag<std::string>, tsu& me) {
type_ids.push_back(me.get_type_id<std::string>());
}
));
assert(type_ids == vector<int>({0,1,2}));
!*/
{
return detail::overloaded_helper<std::decay_t<T>...>{std::forward<T>(t)...};
}
}
#endif //DLIB_OVERLOADED_H_

View File

@ -42,6 +42,7 @@ set (tests
conditioning_class_c.cpp
conditioning_class.cpp
config_reader.cpp
constexpr_if.cpp
correlation_tracker.cpp
crc32.cpp
create_iris_datafile.cpp

206
dlib/test/constexpr_if.cpp Normal file
View File

@ -0,0 +1,206 @@
// Copyright (C) 2022 Davis E. King (davis@dlib.net)
// License: Boost Software License See LICENSE.txt for the full license.
#include <string>
#include <dlib/constexpr_if.h>
#include <dlib/invoke.h>
#include "tester.h"
namespace
{
using namespace test;
using namespace dlib;
logger dlog("test.constexpr_if");
struct A
{
int i;
};
struct B
{
float f;
};
struct C
{
std::string str;
};
struct D
{
int i;
void set_i(int j) {i = j;}
};
template<typename T>
auto handle_type_and_return1(T obj)
{
return switch_(types_<T>{},
[&](types_<A>, auto _) {
return _(obj).i;
},
[&](types_<B>, auto _) {
return _(obj).f;
},
[&](types_<C>, auto _) {
return _(obj).str;
},
[&](auto...) {
printf("Don't know what this type is\n");
}
);
}
template<typename T>
auto handle_type_and_return2(T obj)
{
return switch_(bools(std::is_same<T,A>{}, std::is_same<T,B>{}, std::is_same<T,C>{}),
[&](true_t, auto, auto, auto _) {
return _(obj).i;
},
[&](auto, true_t, auto, auto _) {
return _(obj).f;
},
[&](auto, auto, true_t, auto _) {
return _(obj).str;
},
[&](auto...) {
printf("Don't know what this type is\n");
}
);
}
void test_switch_type()
{
A a{1};
B b{2.5f};
C c{"hello there!"};
{
auto ret = handle_type_and_return1(a);
static_assert(std::is_same<decltype(ret), int>::value, "failed test");
DLIB_TEST(ret == a.i);
}
{
auto ret = handle_type_and_return2(a);
static_assert(std::is_same<decltype(ret), int>::value, "failed test");
DLIB_TEST(ret == a.i);
}
{
auto ret = handle_type_and_return1(b);
static_assert(std::is_same<decltype(ret), float>::value, "failed test");
DLIB_TEST(ret == b.f);
}
{
auto ret = handle_type_and_return2(b);
static_assert(std::is_same<decltype(ret), float>::value, "failed test");
DLIB_TEST(ret == b.f);
}
{
auto ret = handle_type_and_return1(c);
static_assert(std::is_same<decltype(ret), std::string>::value, "failed test");
DLIB_TEST(ret == c.str);
}
{
auto ret = handle_type_and_return2(c);
static_assert(std::is_same<decltype(ret), std::string>::value, "failed test");
DLIB_TEST(ret == c.str);
}
}
template <typename Func, typename... Args>
bool try_invoke(Func&& f, Args&&... args)
{
return switch_(bools(is_invocable<Func, Args...>{}),
[&](true_t, auto _) {
_(std::forward<Func>(f))(std::forward<Args>(args)...);
return true;
},
[](auto...) {
return false;
}
);
}
void test_try_invoke()
{
int value = 0;
auto foo = [&]{ value++; };
auto bar = [&](int i) { value += i; };
auto baz = [&](int i, int j) { value += (i+j); };
DLIB_TEST(try_invoke(foo));
DLIB_TEST(value == 1);
DLIB_TEST(!try_invoke(foo, 1));
DLIB_TEST(value == 1);
DLIB_TEST(!try_invoke(foo, 1, 2));
DLIB_TEST(value == 1);
DLIB_TEST(!try_invoke(bar));
DLIB_TEST(value == 1);
DLIB_TEST(try_invoke(bar, 1));
DLIB_TEST(value == 2);
DLIB_TEST(!try_invoke(bar, 1, 2));
DLIB_TEST(value == 2);
DLIB_TEST(!try_invoke(baz));
DLIB_TEST(value == 2);
DLIB_TEST(!try_invoke(baz, 1));
DLIB_TEST(value == 2);
DLIB_TEST(try_invoke(baz, 1, 2));
DLIB_TEST(value == 5);
}
template<typename T>
using set_i_pred = decltype(std::declval<T>().set_i(int{}));
template<typename T>
bool try_set_i(T& obj, int i)
{
return switch_(bools(is_detected<set_i_pred, T>{}),
[&](true_t, auto _) {
_(obj).set_i(i);
return true;
},
[](auto...){
return false;
}
);
}
void test_set_i()
{
A a{1};
D d{1};
DLIB_TEST(!try_set_i(a, 2));
DLIB_TEST(a.i == 1);
DLIB_TEST(try_set_i(d, 2));
DLIB_TEST(d.i == 2);
}
class constexpr_if_test : public tester
{
public:
constexpr_if_test (
) : tester ("test_constexpr_if",
"Runs tests on the C++14 approximation of C++17 if constexpr() statements but better.")
{}
void perform_test (
)
{
test_switch_type();
test_try_invoke();
test_set_i();
}
} a;
}

View File

@ -10,6 +10,7 @@
#include <functional>
#include "../serialize.h"
#include "../utility.h"
#include "../overloaded.h"
namespace dlib
{
@ -525,7 +526,7 @@ namespace dlib
void for_each_type_impl(
F&& f,
TSU&& tsu,
dlib::index_sequence<I...>
std::index_sequence<I...>
)
{
using Tsu = typename std::decay<TSU>::type;
@ -550,7 +551,7 @@ namespace dlib
{
using Tsu = typename std::decay<TSU>::type;
static constexpr std::size_t Size = type_safe_union_size<Tsu>::value;
detail::for_each_type_impl(std::forward<F>(f), std::forward<TSU>(tsu), dlib::make_index_sequence<Size>{});
detail::for_each_type_impl(std::forward<F>(f), std::forward<TSU>(tsu), std::make_index_sequence<Size>{});
}
template<typename F, typename TSU>
@ -638,50 +639,6 @@ namespace dlib
throw serialization_error(e.info + "\n while deserializing an object of type type_safe_union");
}
}
#if __cplusplus >= 201703L
template<typename ...Base>
struct overloaded_helper : Base...
{
template<typename... T>
overloaded_helper(T&& ... t) : Base{std::forward<T>(t)}... {}
using Base::operator()...;
};
#else
template<typename Base, typename ... BaseRest>
struct overloaded_helper: Base, overloaded_helper<BaseRest...>
{
template<typename T, typename ... TRest>
overloaded_helper(T&& t, TRest&& ...trest) :
Base{std::forward<T>(t)},
overloaded_helper<BaseRest...>{std::forward<TRest>(trest)...}
{}
using Base::operator();
using overloaded_helper<BaseRest...>::operator();
};
template<typename Base>
struct overloaded_helper<Base> : Base
{
template<typename T>
overloaded_helper<Base>(T&& t) : Base{std::forward<T>(t)}
{}
using Base::operator();
};
#endif //__cplusplus >= 201703L
template<typename... T>
overloaded_helper<typename std::decay<T>::type...> overloaded(T&&... t)
{
return overloaded_helper<typename std::decay<T>::type...>{std::forward<T>(t)...};
}
}
#endif // DLIB_TYPE_SAFE_UNIOn_h_

View File

@ -446,68 +446,6 @@ namespace dlib
!*/
// ----------------------------------------------------------------------------------------
template<typename... T>
overloaded_helper<typename std::decay<T>::type...> overloaded(T&&... t)
{
return overloaded_helper<typename std::decay<T>::type...>{std::forward<T>(t)...};
}
/*!
This is a helper function for passing many callable objects (usually lambdas)
to either apply_to_contents(), visit() or for_each(), that combine to make a complete
visitor. A picture paints a thousand words:
using tsu = type_safe_union<int,float,std::string>;
tsu a = std::string("hello there");
std::string result;
a.apply_to_contents(overloaded(
[&result](int) {
result = std::string("int");
},
[&result](float) {
result = std::string("float");
},
[&result](const std::string& item) {
result = item;
}
));
assert(result == "hello there");
result = "";
result = visit(overloaded(
[](int) {
return std::string("int");
},
[](float) {
return std::string("float");
},
[](const std::string& item) {
return item;
}
), a);
assert(result == "hello there");
std::vector<int> type_ids;
for_each_type(a, overloaded(
[&type_ids](in_place_tag<int>, tsu& me) {
type_ids.push_back(me.get_type_id<int>());
},
[&type_ids](in_place_tag<float>, tsu& me) {
type_ids.push_back(me.get_type_id<float>());
},
[&type_ids](in_place_tag<std::string>, tsu& me) {
type_ids.push_back(me.get_type_id<std::string>());
}
));
assert(type_ids == vector<int>({0,1,2}));
!*/
}
#endif // DLIB_TYPE_SAFE_UNION_KERNEl_ABSTRACT_

View File

@ -12,8 +12,17 @@
namespace dlib
{
// ---------------------------------------------------------------------
#ifdef __cpp_lib_integer_sequence
template<std::size_t... Ints>
using index_sequence = std::index_sequence<Ints...>;
template<std::size_t N>
using make_index_sequence = std::make_index_sequence<N>;
template<class... T>
using index_sequence_for = std::index_sequence_for<T...>;
#else
// ---------------------------------------------------------------------
template<std::size_t... Ints>
struct index_sequence
{
@ -31,54 +40,44 @@ namespace dlib
template<std::size_t N>
struct make_index_sequence
: merge_and_renumber < typename make_index_sequence < N / 2 >::type,
typename make_index_sequence < N - N / 2 >::type > {};
: merge_and_renumber < typename make_index_sequence < N / 2 >::type,
typename make_index_sequence < N - N / 2 >::type > {};
template<> struct make_index_sequence<0> : index_sequence<> {};
template<> struct make_index_sequence<1> : index_sequence<0> {};
template<typename... Ts>
using index_sequence_for = make_index_sequence<sizeof...(Ts)>;
// ---------------------------------------------------------------------
#endif
// ---------------------------------------------------------------------
template <typename First, typename... Rest>
struct are_nothrow_move_constructible
: std::integral_constant<bool, std::is_nothrow_move_constructible<First>::value &&
are_nothrow_move_constructible<Rest...>::value> {};
template<bool First, bool... Rest>
struct And : std::integral_constant<bool, First && And<Rest...>::value> {};
template <typename T>
struct are_nothrow_move_constructible<T> : std::is_nothrow_move_constructible<T> {};
template<bool Value>
struct And<Value> : std::integral_constant<bool, Value>{};
// ---------------------------------------------------------------------
template <typename First, typename... Rest>
struct are_nothrow_move_assignable
: std::integral_constant<bool, std::is_nothrow_move_assignable<First>::value &&
are_nothrow_move_assignable<Rest...>::value> {};
template <typename T>
struct are_nothrow_move_assignable<T> : std::is_nothrow_move_assignable<T> {};
template <typename ...Types>
struct are_nothrow_move_constructible : And<std::is_nothrow_move_constructible<Types>::value...> {};
// ---------------------------------------------------------------------
template <typename First, typename... Rest>
struct are_nothrow_copy_constructible
: std::integral_constant<bool, std::is_nothrow_copy_constructible<First>::value &&
are_nothrow_copy_constructible<Rest...>::value> {};
template <typename T>
struct are_nothrow_copy_constructible<T> : std::is_nothrow_copy_constructible<T> {};
template <typename ...Types>
struct are_nothrow_move_assignable : And<std::is_nothrow_move_assignable<Types>::value...> {};
// ---------------------------------------------------------------------
template <typename First, typename... Rest>
struct are_nothrow_copy_assignable
: std::integral_constant<bool, std::is_nothrow_copy_assignable<First>::value &&
are_nothrow_copy_assignable<Rest...>::value> {};
template <typename ...Types>
struct are_nothrow_copy_constructible : And<std::is_nothrow_copy_constructible<Types>::value...> {};
template <typename T>
struct are_nothrow_copy_assignable<T> : std::is_nothrow_copy_assignable<T> {};
// ---------------------------------------------------------------------
template <typename ...Types>
struct are_nothrow_copy_assignable : And<std::is_nothrow_copy_assignable<Types>::value...> {};
// ---------------------------------------------------------------------
@ -115,21 +114,8 @@ namespace dlib
// ---------------------------------------------------------------------
template <typename First, typename... Rest>
struct are_nothrow_swappable
: std::integral_constant<bool, is_nothrow_swappable<First>::value &&
are_nothrow_swappable<Rest...>::value> {};
template <typename T>
struct are_nothrow_swappable<T> : is_nothrow_swappable<T> {};
// ---------------------------------------------------------------------
template<bool First, bool... Rest>
struct And : std::integral_constant<bool, First && And<Rest...>::value> {};
template<bool Value>
struct And<Value> : std::integral_constant<bool, Value>{};
template <typename ...Types>
struct are_nothrow_swappable : And<is_nothrow_swappable<Types>::value...> {};
// ---------------------------------------------------------------------