You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写避免窄化转换与切片的LosslessInvocable概念?

实现无损失可调用对象概念:避免窄化转换与对象切片

我需要设计一个C++概念,用于约束可调用对象,同时满足核心目标:避免窄化转换、防止对象切片,且兼容鸭子类型模板中合法的可调用场景。


初始代码与问题

注释掉概念约束时,以下代码可正常运行并输出所有内容:

#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 Callable
>
/*
requires
    // 无法接受鸭子类型中合法的可调用对象
    std::invocable<Callable, T>

    // 可编译但未实际约束,且重复函数体代码
    // requires (Value&& val, Callable&& callable) {
    //     std::forward<Callable>(callable)(std::forward<Value>(val).value);
    // }

    // 编译失败:无法推导Arg
    // non_narrowing<T, Arg> && std::invocable<Callable, Arg>

    // 编译失败:无法推导Arg,且重复代码
    // 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>时,无法调用printI32Ref(std::invocable仅检查传递T类型参数的可行性,无法匹配左值引用参数);
  • 启用函数体逻辑的requires约束时,仅能保证编译通过,未真正约束可调用对象,且重复了函数体代码;
  • 引入typename Arg搭配non_narrowing与std::invocable时,会出现Arg无法推导的错误。

需求明确

需要实现LosslessInvocable<Callable, T>概念,满足:

  1. 支持所有合法传递方式:可调用对象接受值传递、const引用传递、左值引用传递、右值引用传递;
  2. 阻止窄化转换:printI16无法编译(int32_t转int16_t属于窄化),printI64可编译(int32_t转int64_t无窄化);
  3. 支持逆变:接受Base&或Base*的可调用对象可用于ValueHolder<Derived>;
  4. 阻止对象切片:接受Base值传递的可调用对象无法用于ValueHolder<Derived>。

解决方案

核心概念实现

通过拆分辅助概念,组合成满足所有需求的LosslessInvocable:

#include <cstdint>
#include <concepts>
#include <print>
#include <type_traits>
#include <utility>
#include <tuple>

// 检查非窄化转换:From可无损失转换为To
template<typename From, typename To>
concept non_narrowing = requires(From f) { To{f}; };

// 检查是否为引用类型
template<typename T>
concept is_reference = std::is_reference_v<T>;

// 检查是否为指针类型
template<typename T>
concept is_pointer = std::is_pointer_v<T>;

// 辅助Trait:获取可调用对象的参数类型
template<typename Callable>
struct function_traits : function_traits<decltype(&Callable::operator())> {};

template<typename Class, typename Ret, typename... Args>
struct function_traits<Ret(Class::*)(Args...) const> {
    using args = std::tuple<Args...>;
};

template<typename Ret, typename... Args>
struct function_traits<Ret(*)(Args...)> {
    using args = std::tuple<Args...>;
};

// 检查可调用对象是否接受T的任意引用类型(左值/右值/const引用)
template<typename Callable, typename T>
concept invocable_with_reference_to =
    std::invocable<Callable, T&> ||
    std::invocable<Callable, const T&> ||
    std::invocable<Callable, T&&>;

// 检查可调用对象是否接受T的基类的引用/指针(逆变,无切片)
template<typename Callable, typename T>
concept invocable_with_contravariant_base =
    std::is_class_v<T> &&
    std::invocable<Callable, T> &&
    // 确保参数是引用或指针,避免切片
    is_reference<std::tuple_element_t<0, typename function_traits<Callable>::args>> ||
    is_pointer<std::tuple_element_t<0, typename function_traits<Callable>::args>>;

// 最终的无损失可调用概念
template<typename Callable, typename T>
concept LosslessInvocable =
    // 情况1:值传递,无窄化,类类型无切片
    (std::invocable<Callable, T> &&
     non_narrowing<T, std::decay_t<std::invoke_result_t<Callable, T>>> &&
     (!std::is_class_v<T> || std::same_as<T, std::decay_t<std::invoke_result_t<Callable, T>>>)) ||
    // 情况2:接受T的任意引用传递
    invocable_with_reference_to<Callable, T> ||
    // 情况3:接受T的基类的引用/指针(逆变,无切片)
    invocable_with_contravariant_base<Callable, T>;

修改后的callOnValue函数

template<
    typename Value,
    typename T = std::remove_reference_t<Value>::ValueType,
    typename Callable
>
requires LosslessInvocable<Callable, T>
auto callOnValue(Value&& val, Callable&& callable)
{
    return std::forward<Callable>(callable)(std::forward<Value>(val).value);
}

验证效果

  • 支持所有传递方式:printI32、printI32Ref、printI32RefRef、printConstI32Ref均可正常调用;
  • 阻止窄化转换:callOnValue(i, printI16)编译失败,callOnValue(i, printI64)编译通过;
  • 支持逆变:callOnValue(d, printConstBaseRef)编译通过;
  • 阻止对象切片:callOnValue(d, printBase)编译失败。

内容的提问来源于stack exchange,提问作者Nil Admirari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 08:57:04