【问题标题】:Pretty sfinae with static_assert漂亮的 sfinae 与 static_assert
【发布时间】:2017-01-16 06:14:42
【问题描述】:

我正在尝试创建一个将注册接收器的事件管理器。为此,我希望能够构造一个带有给定参数的std::function。但是,我希望最终用户可以轻松理解错误。我曾想过使用 SFINAE 和依赖于类型的 static_assert 来执行此操作,但我遇到了麻烦,因为这两个函数在有效输入上变得模棱两可。此外,我希望用户可以收到多个错误原因。由于有两个故障点(提供无效的函子并提供错误的事件类型),我希望总共有 3 个函数,第一个是正确输入的函数,然后是不正确的输入(而不是 4 个函数)每个状态组合)。

这可以用 c++17 的 if constexpr 解决,但我的目标平台是 c++14,所以需要使用其他方法。

我当前的尝试(仅检查一种错误状态):

template <typename Event, typename Func>
auto register(Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}

template <typename Event, typename Func>
void register(Func &&) {
    static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}

meta::delay_v 等于false,但取决于其参数,因此在调用函数之前不会触发static_assert


一个更复杂的用例是

template <typename Event, typename Func>
auto register(Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func))
            ,meta::is_in_tuple<Event, Events_Tuple>
            ,void()) {}

因此,如果第一个测试失败(func_t 构造),那么我们将对此进行 static_assert,如果第二个测试失败,我们将对此进行static_assert。因此,如果第一个测试失败,无论第二个测试如何,我们都会失败一些静态断言。然后,如果第一个测试通过,我们将打印第二个测试失败的信息。不必重写测试将是一个非常好的奖励。

【问题讨论】:

  • 使用标签调度+简单的重载,或者实现/使用detection idiom
  • @ildjarn 有了检测习语,我不需要为 2 个可能的错误点提供 4 个函数吗?因为对于每个函数,我都必须检查所有错误点的一个特定组合。除非我没有看到让我只有 3 个函数来处理 2 个错误(以及 4 个函数来处理 3 个错误)。
  • 您将编写 1 + ErrorN 函数;分派到正确错误/成功函数的公共 API 是通用样板,您只需编写一次并用于每组函数。
  • 是的,我现在明白了,谢谢。

标签: c++ templates c++14 sfinae


【解决方案1】:

当条件满足时,它们实际上是模棱两可的,因为两者都是有效的。
只有第一个函数具有可以禁用它的 sfinae 表达式,因此第二个函数始终是一个可行的解决方案(即满足条件时的模棱两可的解决方案)。

您可以这样做:

template <typename Event, typename Func>
auto register(int, Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}

template <typename Event, typename Func>
void register(char, Func &&) {
    static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}

template <typename Event, typename Func>
void register(Func &&func) {
    register<Event>(0, std::forward<Func>(func));
}

在这种情况下,0(即int)将强制编译器选择第一个函数并尝试它。如果有效,则没有歧义(第二个想要char),否则0 可以转换为char 并用于调用第二个函数。

如果你有两个以上的条件,你可以这样做:

template<int N> struct tag: tag<N-1> {};
template<> struct tag<0> {};

template <typename Event, typename Func>
auto register(tag<2>, Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}

template <typename Event, typename Func>
auto register(tag<1>, Func &&func)
-> decltype(func_alternative_t<Event>(std::forward<Func>(func)), void()) {}

template <typename Event, typename Func>
void register(tag<0>, Func &&) {
    static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}

template <typename Event, typename Func>
void register(Func &&func) {
    register<Event>(tag<2>{}, std::forward<Func>(func));
}

解决方案的数量越多,与标签一起使用的数量就越大。适用于int/char 技巧的相同原理在这里起作用。


作为旁注,正如 @StoryTeller 在 cmets 中提到的,请注意 register 是保留关键字,您不应在生产代码中使用它。

【讨论】:

  • 嗯,很有趣。但是想要在多个条件下出现错误怎么办?例如,如果Event 是错误类型并且还用于构造function?这似乎受限于一定数量的错误条件,甚至可能只有 2 个。
  • register 仍然是保留关键字(委员会肯定喜欢保留它们)。只是需要考虑的一点。
  • @StoryTeller 你是对的。我按原样从问题中得到代码,没有注意到它是register。我会提到它。谢谢。
  • @user975989 添加了更多详细信息。
  • 嗯,我认为您误解了我的用例,让我更新问题。
【解决方案2】:

经过一番思考,我找到了另一种结构似乎更好的方法。

template <typename Func, typename... Ts>
decltype(auto) evaluate_detector(std::true_type, Func && f, Ts&&...) {
    return f(true);
}

template <typename Func, typename... Ts>
decltype(auto) evaluate_detector(std::false_type, Func &&, Ts&&... ts) {
    return evaluate_detector(std::forward<Ts>(ts)...);
}

template <typename Event, typename Func>
void register(Func &&func) {
    using can_construct = std::is_constructable<func_t<Event>, Func>;
    using proper_event = meta::is_in_tuple<Event, Events_Tuple>;
    evaluate_detector(meta::and<can_construct, proper_event>{},
        [&](auto){/*do proper thing*/};
        meta::not<can_construct>{},
        [](auto delay){static_assert(meta::delay_v<decltype(delay)>, "can't construct"},
        meta::not<proper_event>{},
        [](auto delay){static_assert(meta::delay_v<decltype(delay)>, "improper event"});
}

优点是将所有错误状态都集中在一个中心位置,而不必创建许多被覆盖的函数。这就是我设想要使用的检测器习语的方式。 can_constructproper_event 的类型评估为 std::true_typestd::false_type,或者继承这些类型的东西,所以我们仍然有重载决议,但以通用方式完成。

【讨论】:

    【解决方案3】:

    注意:这开始是对the OP's answer 的评论,但变得有点大;为衍生品道歉。

    我建议进行以下重组:

    namespace detail {
        template<typename PredT, typename F>
        struct fail_cond {
            using pred_type = PredT;
            F callback;
        };
        struct success_tag { };
    
        template<typename F>
        constexpr decltype(auto) eval_if(int, fail_cond<success_tag, F>&& fc) {
            return fc.callback();
        }
    
        template<
            typename FC, typename... FCs,
            typename PredT = typename std::decay_t<FC>::pred_type,
            std::enable_if_t<std::is_base_of<std::false_type, PredT>{}, int> = 0
        >
        constexpr decltype(auto) eval_if(int, FC&& fc, FCs&&...) {
            return fc.callback(PredT{});
        }
    
        template<typename FC, typename... FCs>
        constexpr decltype(auto) eval_if(long, FC&&, FCs&&... fcs) {
            return detail::eval_if(0, std::move(fcs)...);
        }
    }
    
    template<typename PredT, typename F, typename = std::result_of_t<F&(std::true_type)>>
    constexpr detail::fail_cond<PredT, F> fail_cond(F&& failure_cb) {
        return {std::forward<F>(failure_cb)};
    }
    
    template<typename F, typename... PredTs, typename... Fs>
    constexpr decltype(auto) eval_if(F&& success_cb, detail::fail_cond<PredTs, Fs>&&... fcs) {
        return detail::eval_if(
            0, std::move(fcs)...,
            detail::fail_cond<detail::success_tag, F>{std::forward<F>(success_cb)}
        );
    }
    

    现在的用法如下所示:

    template<typename Event, typename Func>
    decltype(auto) register(Func&& func) {
       using can_construct = std::is_constructible<func_t<Event>, Func&&>;
       using proper_event = meta::is_in_tuple<Event, Events_Tuple>;
       return eval_if(
          [&]() { /*do proper thing*/ },
          fail_cond<can_construct>([](auto pred) { static_assert(pred, "can't construct"); }),
          fail_cond<proper_event>([](auto pred) { static_assert(pred, "improper event"); })
       );
    }
    
    // or ...
    
    template<typename Event, typename Func>
    decltype(auto) register(Func&& func) {
       return eval_if(
          [&]() { /*do proper thing*/ },
          fail_cond<std::is_constructible<func_t<Event>, Func&&>>(
              [](auto pred) { static_assert(pred, "can't construct"); }
          ),
          fail_cond<meta::is_in_tuple<Event, Events_Tuple>>(
              [](auto pred) { static_assert(pred, "improper event"); }
          )
       );
    }
    

    Online Demo

    该演示虽然设计得很痛苦,但显示了编译时运行时失败行为的可能性(n.b.失败回调可以返回值)。还证明了传递给失败回调的值是失败的谓词的一个实例,这允许可能更丰富的失败行为并减少static_asserts 所需的样板。

    【讨论】:

    • 这真的很好很干净,绝对是对我的回答的改进。后端有更多的机器,但这很好,因为前端要好得多。
    • MSVC 14/2015 似乎无法通过 sfinae 选择第一个重载函数(我相信 enable_if_t 永远不会起作用)所以我想知道是否有解决方法。
    • 通过将is_base_of&lt;&gt;{} 替换为is_base_of&lt;&gt;::value 并始终传递false_type 而不是PredT 来修复它。
    • @user975989 :这失去了答案一半的可调试性。 :-] 我无法在本地测试 ATM,但 this 可以工作,并且只涉及更改 ERROR 宏。
    • 很公平,它适合我的需要,但是可以通过更多的输入/宏来完成。
    猜你喜欢
    • 2021-04-24
    • 2015-05-27
    • 2012-08-12
    • 2020-12-05
    • 2020-11-21
    • 1970-01-01
    • 2021-06-22
    • 2016-11-30
    • 2020-08-13
    相关资源
    最近更新 更多