我想要一个可调用的概念,以避免缩小转换和切片。在第一次尝试编写它时,我发现简单地使用
std::invocable<T>
会拒绝在鸭子类型模板中可以使用的可调用对象。
以下代码在注释掉概念时打印所有内容:
#include <cstdint>
#include <concepts>
#include <print>
#include <type_traits>
template<typename T>
struct ValueHolder {
using ValueType = T;
T value;
};
template <typename From, typename To>
concept non_narrowing = requires(From f) { To{f}; };
template<
typename Value,
typename T = std::remove_reference_t<Value>::ValueType,
// typename Arg,
typename Callable
>
/*
requires
// Fails to accept callables valid in a duck typed template.
std::invocable<Callable, T>
// Compiles, but doesn't actually constrain the Arg.
// Duplicates code from the function itself.
requires (Value&& val, Callable&& callable) {
std::forward<Callable>(callable)(std::forward<Value>(val).value);
}
// Doesn't compile: can't deduce Arg.
// std::non_narrowing<T, Arg>
// && std::invocable<Callable, Arg>
// Doesn't compile: can't deduce Arg.
// Duplicates code from the function itself.
// requires (Value&& val, Callable&& callable) {
// std::forward<Callable>(callable)(non_narrowing<decltype(std::forward<Value>(val).value), Arg>);
// }
*/
auto callOnValue(Value&& val, Callable&& callable)
{
std::forward<Callable>(callable)(std::forward<Value>(val).value);
}
void printI32(std::int32_t) { std::println("i32"); }
void printI32Ref(std::int32_t&) { std::println("i32&"); }
void printI32RefRef(std::int32_t&&) { std::println("i32&&"); }
void printConstI32Ref(const std::int32_t &) { std::println("const i32&"); }
void printI16(std::int16_t) { std::println("i16"); }
void printI64(std::int64_t) { std::println("i64"); }
struct Base {};
struct Derived: public Base {};
void printBase(Base) { std::println("Base"); }
void printConstBaseRef(const Base&) { std::println("const Base&"); }
void printDerived(Derived) { std::println("Derived"); }
void printConstDerivedRef(const Derived&) { std::println("const Derived&"); }
int main()
{
ValueHolder i{static_cast<std::int32_t>(42)};
callOnValue(i, printI32);
callOnValue(i, printI32Ref);
callOnValue(ValueHolder{static_cast<std::int32_t>(42)}, printI32RefRef);
const auto ci{i};
callOnValue(ci, printI32);
callOnValue(ci, printConstI32Ref);
callOnValue(i, printI16);
callOnValue(i, printI64);
ValueHolder d{Derived{}};
callOnValue(d, printBase);
callOnValue(d, printConstBaseRef);
callOnValue(d, printDerived);
callOnValue(d, printConstDerivedRef);
}
取消评论
requires
std::invocable<Callable, T>
无法拨打电话
printIntRef
。
取消评论
requires (Value&& val, Callable&& callable) {
std::forward<Callable>(callable)(std::forward<Value>(val).value);
}
编译,但实际上并不约束
callable
,看起来像是将函数体写了两次。
取消注释
typename Arg
以及任一
std::non_narrowing<T, Arg>
&& std::invocable<Callable, Arg>
或
requires (Value&& val, Callable&& callable) {
std::forward<Callable>(callable)(non_narrowing<decltype(std::forward<Value>(val).value), Arg>);
}
导致无法推断出
Arg
。
如何写一个概念
LosslessInvocable<PassByWhateverWorksInDuckTypedTemplates<T>>
:
callable
接受按值传递、const ref 传递、左值传递 ref 传递以及右值传递 ref-ref 传递)。printI16
无法编译,但 printI64
可以。Base&
上调用接受 Base*
或 ValueHolder<Derived>
的可调用对象。Base
无法编译 ValueHolder<Derived>
。问题在于你对自己真正想做的事情感到困惑。
首先,
callOnValue
应该是这样的
void callOnValue(Value&& val, Callable&& callable)
{
std::forward<Callable>(callable)(std::forward_like<Value>(val.value));
}
否则,您会在正确转发会员值时遇到问题。
std::forward_like
来自 C++23,因此您可以使用替代实现:
template<class T, class U>
constexpr auto&& forward_like(U&& x) noexcept
{
constexpr bool is_adding_const = std::is_const_v<std::remove_reference_t<T>>;
if constexpr (std::is_lvalue_reference_v<T&&>)
{
if constexpr (is_adding_const)
return std::as_const(x);
else
return static_cast<U&>(x);
}
else
{
if constexpr (is_adding_const)
return std::move(std::as_const(x));
else
return std::move(x);
}
}
第二个问题是模板中的
Value
可能是常量,所以要引用ValueType
的Value
最好这样写。
typename T = std::remove_cvref_t<Value>::ValueType
第三个问题是如何测试是否可以正确调用。怎么做呢?您需要将
Value
的特征复制到 T
并测试结果的可调用性。您可以通过自定义类型特征来做到这一点:
template<class T, class U>
struct copy_traits
{
using type = decltype(forward_like<T>(std::declval<U>()));
};
template<class T, class U>
using copy_traits_t = typename copy_traits<T,U>::type;
其中
forward_like
如上所述,并通过 std::invocable<Callable, copy_traits_t<Value, T> >
测试其可调用性
你会得到这样的代码:
#include <cstdint>
#include <concepts>
#include <iostream>
#include <type_traits>
#include <utility>
template<typename T>
struct ValueHolder {
using ValueType = T;
T value;
};
template<class T, class U>
constexpr auto&& forward_like(U&& x) noexcept
{
constexpr bool is_adding_const = std::is_const_v<std::remove_reference_t<T>>;
if constexpr (std::is_lvalue_reference_v<T&&>)
{
if constexpr (is_adding_const)
return std::as_const(x);
else
return static_cast<U&>(x);
}
else
{
if constexpr (is_adding_const)
return std::move(std::as_const(x));
else
return std::move(x);
}
}
template<class T, class U>
struct copy_traits
{
using type = decltype(forward_like<T>(std::declval<U>()));
};
template<class T, class U>
using copy_traits_t = typename copy_traits<T,U>::type;
template<
typename Value,
typename Callable,
typename T = std::remove_cvref_t<Value>::ValueType
>
requires (std::invocable<Callable, copy_traits_t<Value, T> >)
void callOnValue(Value&& val, Callable&& callable)
{
std::forward<Callable>(callable)(forward_like<Value>(val.value));
}
void printI32(std::int32_t) { std::cout << "i32\n"; }
void printI32Ref(std::int32_t&) { std::cout << "i32&\n"; }
void printI32RefRef(std::int32_t&&) {std::cout << "i32&&\n"; }
void printConstI32Ref(const std::int32_t &) {std::cout << "const i32&\n"; }
void printI16(std::int16_t) { std::cout << "i16\n"; }
void printI64(std::int64_t) { std::cout << "i16\n"; }
int main()
{
ValueHolder i{static_cast<std::int32_t>(42)};
callOnValue(i, printI32);
callOnValue(i, printI32Ref);
callOnValue(ValueHolder{static_cast<std::int32_t>(42)}, printI32RefRef);
const auto ci{i};
callOnValue(ci, printI32);
callOnValue(ci, printConstI32Ref);
callOnValue(i, printI16);
callOnValue(i, printI64);
}
或者,您也可以编写一个自定义概念:
requires (requires (Value&& val, Callable&& callable)
{
{std::forward<Callable>(callable)(forward_like<Value>(val.value))};
})