std::可调用,无需缩小转换和切片

问题描述 投票:0回答:1

我想要一个可调用的概念,以避免缩小转换和切片。在第一次尝试编写它时,我发现简单地使用

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>>

  1. 允许复制所有默认传递(即
    callable
    接受按值传递、const ref 传递、左值传递 ref 传递以及右值传递 ref-ref 传递)。
  2. 防止缩小转换:上面的
    printI16
    无法编译,但
    printI64
    可以。
  3. 接受逆变可调用对象,即可以在
    Base&
    上调用接受
    Base*
    ValueHolder<Derived>
    的可调用对象。
  4. 防止切片:按值传递可调用接受
    Base
    无法编译
    ValueHolder<Derived>
c++ templates c++-concepts
1个回答
-1
投票

问题在于你对自己真正想做的事情感到困惑。

首先,

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))};
})
© www.soinside.com 2019 - 2024. All rights reserved.