【问题标题】:How to call a templated function if it exists, and something else otherwise?如果存在,如何调用模板化函数,否则如何调用?
【发布时间】:2010-11-26 01:12:21
【问题描述】:

我想做类似的事情

template <typename T>
void foo(const T& t) {
   IF bar(t) would compile
      bar(t);
   ELSE
      baz(t);
}

我认为使用enable_if 的东西可以在这里完成工作,将foo 分成两部分,但我似乎无法弄清楚细节。实现这一目标的最简单方法是什么?

【问题讨论】:

    标签: c++ templates sfinae


    【解决方案1】:

    对名称bar 进行了两次查找。一种是在foo 的定义上下文中的非限定查找。另一种是在每个实例化上下文中的参数相关查找(但每个实例化上下文的查找结果不允许改变两个不同实例化上下文之间的行为)。

    要获得所需的行为,您可以在 fallback 命名空间中定义一个回退函数,该函数返回一些唯一类型

    namespace fallback {
      // sizeof > 1
      struct flag { char c[2]; };
      flag bar(...);
    }
    

    如果没有其他匹配项,将调用 bar 函数,因为省略号的转换成本最低。现在,通过 fallback 的 using 指令将候选对象包含到您的函数中,以便将 fallback::bar 作为候选对象包含到对 bar 的调用中。

    现在,要查看对bar 的调用是否解析为您的函数,您将调用它,并检查返回类型是否为flag。否则选择的函数的返回类型可能是 void,因此您必须做一些逗号运算符技巧来解决这个问题。

    namespace fallback {
      int operator,(flag, flag);
    
      // map everything else to void
      template<typename T> 
      void operator,(flag, T const&);
    
      // sizeof 1
      char operator,(int, flag);
    }
    

    如果我们的函数被选中,那么逗号运算符调用将返回对int 的引用。如果不是,或者如果所选函数返回void,则调用将依次返回void。然后,如果选择了我们的后备,则以 flag 作为第二个参数的下一次调用将返回 sizeof 1 的类型,如果选择了大于 1 的 sizeof(将使用内置逗号运算符,因为 void 混合在一起)如果选择了其他东西。

    我们将 sizeof 和 delegate 与一个结构进行比较。

    template<bool>
    struct foo_impl;
    
    /* bar available */
    template<>
    struct foo_impl<true> {
      template<typename T>
      static void foo(T const &t) {
        bar(t);
      }
    };
    
    /* bar not available */
    template<>
    struct foo_impl<false> {
      template<typename T>
      static void foo(T const&) {
        std::cout << "not available, calling baz...";
      }
    };
    
    template <typename T>
    void foo(const T& t) {
       using namespace fallback;
    
       foo_impl<sizeof (fallback::flag(), bar(t), fallback::flag()) != 1>
         ::foo(t);
    }
    

    如果现有函数也有省略号,则此解决方案不明确。但这似乎不太可能。使用后备测试:

    struct C { };
    int main() {
      // => "not available, calling baz..."
      foo(C());
    }
    

    如果使用参数相关查找找到候选者

    struct C { };
    void bar(C) {
      std::cout << "called!";
    }
    int main() {
      // => "called!"
      foo(C());
    }
    

    为了在定义上下文中测试非限定查找,让我们在foo_implfoo之上定义以下函数(将foo_impl模板放在foo之上,因此它们具有相同的定义上下文)

    void bar(double d) {
      std::cout << "bar(double) called!";
    }
    
    // ... foo template ...
    
    int main() {
      // => "bar(double) called!"
      foo(12);
    }
    

    【讨论】:

    • 天哪! O_o 您是从哪里想到以这种方式使用逗号运算符的? (弄清楚你在那里做了什么本身就花了 15 分钟......)
    • 我在一些 usenet 帖子中读到过它:groups.google.com/group/comp.lang.c++.moderated/tree/browse_frm/…
    • @Faisal Vali,谢谢。我没有只用一个op, 调用来做到这一点,因为有些类型会重载模板化op, 并且接受标志作为第二个参数(比如del &lt;&lt; a, b;,它有一个delim operator,(delim,T const&amp;);)。所以这样做(bar(t), flag()) 会产生歧义。我们也无法交换订单​​,因为bar(t) 可能有类型void。当然,将op, 接受标志作为第一个参数的可能性要小得多-因此是三向的事情:)
    • 嗯 - 好点 - 但这里有一个替代方案 - 不确定是否每个人都同意它更简单但打字更少;)我们可以尝试并尝试添加两个额外的逗号重载来劫持 ADL,而不是通过添加块声明来关闭 ADL。所以在'using namespace fallback;'之后添加以下内容: using fallback::operator,;无效运算符,(标志,int);然后你可以做 sizeof(bar(t),flag()) 我觉得不受惩罚。块声明块 ADL(隐藏提升或全局操作),永远不会被调用,不隐藏正确的 ',op' 也不隐藏内置的 ',op' - 你的想法?
    • @Faisal,好主意 :) 但我认为停止 ADL 的非使用块声明是 c++0x 的事情 :( 尽管如此,GCC 还是做了 ADL,并且开始包括“BETA 2003 年 8 月 4 日”。(我也觉得那些声明在 C++03 中停用了 ADL,但似乎他们在以后的缺陷修复或其他东西中添加了这一点)。好点,让我看看我们是否可以(ab) - 使用成员函数声明 :)
    【解决方案2】:

    litb has given you a very good answer。但是,我想知道,考虑到更多的上下文,我们是否不能想出一些不那么通用但也更少的东西,嗯,详细说明

    比如T可以是什么类型?任何事物?几种类型?您可以控制的非常受限的集合?您设计的某些类与函数foo 结合使用?鉴于后者,你可以简单地把像

    typedef boolean<true> has_bar_func;
    

    进入类型,然后基于此切换到不同的foo 重载:

    template <typename T>
    void foo_impl(const T& t, boolean<true> /*has_bar_func*/);
    template <typename T>
    void foo_impl(const T& t, boolean<false> /*has_bar_func*/);
    
    template <typename T>
    void foo(const T& t) {
      foo_impl( t, typename T::has_bar_func() );
    }
    

    另外,bar/baz 函数可以有任何签名,是否有一些限制集,或者只有一个有效的签名?如果是后者,litb 的(优秀的)后备想法,结合使用sizeof 的元函数可能会更简单一些。但是这个我没有探索过,所以只是一个想法。

    【讨论】:

    • +1 好点,我想。在做太复杂的事情之前,他应该先考虑一下这些东西。像 swap 函数一样使用 ADL 也很有用。无论如何,谢谢你的赞美:)
    • 嗯,具有barT 集合并不是完全任意的,但它不仅仅是几种类型。另外,为不同类型专门设置has_bar_func 将是(小但重要的)代码重复。虽然 litb 的想法非常聪明,但并不是那么“精心”(我认为 boost 中有很多例子要复杂得多)
    • @litb:当我在一个详尽解释的答案顶部看到struct flag { char c[2]; }; 时,我知道这是你的,尽管你的名字仍然没有出现在屏幕上。 :)
    • 更重要的是,这不是第一次出现这样的模式。过去,我做过类似你建议的事情,但这有点乏味和不雅。这一次,我希望(并且得到了!)一个“简单”而优雅的解决方案。
    • +1,我喜欢这种干净简单的方法。我可以建议创建一个单独的特征类,例如foo_traits,用于包含 has_bar_func?这样你也可以处理基本类型。
    【解决方案3】:

    我认为 litb 的解决方案有效,但过于复杂。原因是他引入了一个函数fallback::bar(...),它充当“最后的函数”,然后竭尽全力不调用它。为什么?看来我们有一个完美的行为:

    namespace fallback {
        template<typename T>
        inline void bar(T const& t, ...)
        {
            baz(t);
        }
    }
    template<typename T>
    void foo(T const& t)
    {
        using namespace fallback;
        bar(t);
    }
    

    但正如我在对 litb 原始帖子的评论中指出的那样,bar(t) 无法编译的原因有很多,我不确定这个解决方案是否能处理相同的情况。它肯定会在private bar::bar(T t) 上失败

    【讨论】:

    • 这与 j_random_hacker 的代码具有相同的行为。如果没有传递给省略号的相应参数,则在使用其他函数对重载进行排序时不考虑省略号。 (13.3.213.3.3)。
    【解决方案4】:

    如果您愿意限制自己使用 Visual C++,可以使用 __if_exists__if_not_exists 语句。

    在紧要关头很方便,但特定于平台。

    【讨论】:

    • 不过,这似乎没有考虑调用是否真的会编译。考虑调用“foo(1)”,但存在函数“bar(char*)”。
    • 有趣,我不知道这些。 +1(尽管 litb 的批评是正确的)。
    【解决方案5】:

    编辑:我说得太早了! litb's answer 展示了如何实际做到这一点(以您的理智为代价... :-P)

    不幸的是,我认为检查“是否会编译”的一般情况超出了function template argument deduction + SFINAE 的范围,这是处理这些东西的常用技巧。我认为你能做的最好的就是创建一个“备份”功能模板:

    template <typename T>
    void bar(T t) {   // "Backup" bar() template
        baz(t);
    }
    

    然后将foo()改为简单:

    template <typename T>
    void foo(const T& t) {
        bar(t);
    }
    

    这适用于大多数情况。因为bar() 模板的参数类型是T,与任何其他名为bar() 的函数或函数模板相比,它将被视为“不太专业”,因此在重载期间将优先于该预先存在的函数或函数模板解决。除了:

    • 如果预先存在的bar() 本身是一个函数模板,它采用T 类型的模板参数,则会出现歧义,因为这两个模板都不比另一个模板更专业,编译器会报错。
    • 隐式转换也不起作用,并且会导致难以诊断的问题:假设有一个预先存在的bar(long),但调用了foo(123)。在这种情况下,编译器会悄悄地选择用T = int 实例化“备份”bar() 模板,而不是执行int-&gt;long 提升,即使后者本来可以编译并正常工作!

    简而言之:没有简单、完整的解决方案,而且我很确定甚至没有一个棘手的、完整的解决方案。 :(

    【讨论】:

      【解决方案6】:
      //default
      
      ////////////////////////////////////////// 
          template <class T>
          void foo(const T& t){
              baz(t);
          }
      
      //specializations
      //////////////////////////////////////////  
      
          template <>
          void foo(const specialization_1& t){
              bar(t);
          }
          ....
          template <>
          void foo(const specialization_n& t){
              bar(t);
          }
      

      【讨论】:

      • +1 纯粹是为了简单,但与 sbi 的解决方案一样,这里的问题是您需要使用支持 bar() 的类型列表来保持专业化列表的最新状态——和是OP试图避免做的事情。
      • 但是这样做就像处理独立的 std::ostream& operator
      • 1)谁说“如果它会编译”实际上会得到你真正想要的代码? 2) 类外部的更改可以在没有任何指示的情况下更改目标代码语义(向一个类添加构造函数,从另一个类中删除构造函数)。此外,将显式特化放在一个标题中可能会增加歧义并有助于上述 2)。
      • 我完全同意您在上一条评论中的两个观点——这很容易导致维护地狱。但同时我认为(a)这是一个有趣的问题,询问是否(或在多大程度上)可以实现 OP 要求的 if_this_would_compile(...) 构造 - 并且(b)我认为在某些情况下它为设计带来的增加的干燥度可能超过增加的“远距离动作”复杂性。 (许多类似但不太通用的构造,例如模板 arg 推导 + SFINAE,在 Boost 等备受推崇的库中已经违反了您的 (2)。)
      【解决方案7】:

      你不能在这里对 foo 使用完全专业化(或重载)吗?通过说有函数模板调用 bar 但对于某些类型完全专门化它来调用 baz?

      【讨论】:

      • 并非所有受“是否编译”检查的属性都可以通过调用模板函数时发生的类型推断来验证。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-10-07
      • 1970-01-01
      • 1970-01-01
      • 2013-04-21
      • 2018-11-22
      • 1970-01-01
      • 2012-06-21
      相关资源
      最近更新 更多