【问题标题】:C++ SFINAE not failingC++ SFINAE 没有失败
【发布时间】:2021-02-18 14:03:24
【问题描述】:

代码:

#include <iostream>


using std::nullptr_t;

template<typename... T>
using nullptr_vt = nullptr_t;

struct not_addable{};


template<
  typename T, 
  nullptr_vt<decltype(std::declval<T>() + std::declval<T>())> TSfinae = nullptr>
bool test_addable(int)
{ return true; }

template<typename>
bool test_addable(...)
{ return false; }


int main()
{
  std::cout << std::boolalpha;

  std::cout << test_addable<int>(0) << std::endl;
  std::cout << test_addable<not_addable>(0) << std::endl;

  // Gives error ("invalid operands to binary expression"):
  // nullptr_vt<decltype(std::declval<not_addable>() + std::declval<not_addable>())> a{};
}

我以为这会打印出来:

true
false

,但事实并非如此。它打印:

true 
true

。至少在https://repl.it/@Hrle/sfinaetemplatesuccess 上。

我认为第一个重载中的nullptr_vt&lt;decltype(std::declval&lt;T&gt;() + std::declval&lt;T&gt;())&gt;not_addable 来说是一个错误,它会从重载集中丢弃它,从而选择第二个重载。

如果有默认值,编译器是否有能力丢弃TSfinae的类型?

【问题讨论】:

  • 如果我不得不冒险猜测一下,那将是那里使用的编译器(我无法访问它自己查看),无法解析issue mentioned here。并尝试在旧版本的 Clang 上修复建议的方法,wandbox.org/permlink/8pOrfXpPj0nQA4Of
  • fwiw,我可以用 gcc 重现你的结果,但 clang 是 true false godbolt.org/z/KjdecT
  • 在 gcc 上,如果您将 decltype(std::declval&lt;T&gt;() + std::declval&lt;T&gt;())&gt; TSfinae = nullptr 修改为 decltype(std::declval&lt;T&gt;() + std::declval&lt;T&gt;())&gt;* TSfinae = nullptr 它可以工作...

标签: c++ templates sfinae


【解决方案1】:

我认为第一次重载中的nullptr_vt&lt;decltype(std::declval&lt;T&gt;() + std::declval&lt;T&gt;())&gt; 对于 not_addable 来说是一个错误,它会从重载集中丢弃它,从而选择第二次重载。

这个想法其实很好,问题出在 GCC 和 nullptr_vt

这一行:

nullptr_vt<decltype(std::declval<T>() + std::declval<T>())> TSfinae = nullptr

在 GCC 10.2 上你不希望它在哪里工作,但在 Clang 11.0.1 上是正确的。改成

nullptr_vt<decltype(std::declval<T>() + std::declval<T>())> *TSfinae = nullptr

两者都是正确的,更简单的也是如此

typename TSfinae = nullptr_vt<decltype(std::declval<T>() + std::declval<T>())>
typename _ = decltype(std::declval<T>() + std::declval<T>())

最后是 make_void 技巧

template<typename... T> struct make_nullptr_vt { using type = nullptr_t; };

template<typename T>
using nullptr_vt = typename make_nullptr_vt<T>::type;

也修复了 GCC 上的原始版本。

【讨论】:

  • 非常感谢!我不采用更简单的解决方案的原因是因为用户可以将他们想要的任何东西放在那里(据我所知)并且它会起作用。最后一个是我最喜欢的一个,因为我必须在我的代码中做最少的修改。
【解决方案2】:

这并没有解释问题,也没有假装比@Useless的答案更好,但它是我觉得方便的另一种解决方案。

我将typename 替换为一个整数,以节省一些写作时间,并使用逗号运算符在必要时枚举许多条件。 当然,当必须多次使用相同的条件时,使用 using 的别名声明有助于提高可读性。


编辑

正如@StoryTeller 评论所建议的,如果我们声明一个与最后一个1 结合的operator,,那么1 将被消耗,我们可以在decltype() 中发出一个会使SFINAE 失败的类型. 他建议在1 之前的条件序列中插入void()。 实际上,没有右侧操作数是不可能声明operator, 的。因此,没有任何东西会与void() 结合,最后1 将在decltype() 中发出。 它不像1那么小,但更安全。

/**
  g++ -std=c++17 -o prog_cpp prog_cpp.cpp \
      -pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion \
      -g -O0 -UNDEBUG -fsanitize=address,undefined
**/

#include <iostream>

struct A
{
  A operator+(A r);
  A operator-(A r);
  A operator,(int r); // try to mislead SFINAE
};

struct B
{
  B operator+(B r);
  // no -
};

struct C
{
  // no +
  // no -
};

template<
  typename T, 
  decltype((std::declval<T>()+std::declval<T>()),
           void(),1) =1>
bool test_add(int)
{ return true; }

template<typename>
bool test_add(...)
{ return false; }

template<
  typename T, 
  decltype((std::declval<T>()+std::declval<T>()),
           (std::declval<T>()-std::declval<T>()),
           void(),1) =1>
bool test_add_sub(int)
{ return true; }

template<typename>
bool test_add_sub(...)
{ return false; }

template<typename T>
using has_add =
  decltype((std::declval<T>()+std::declval<T>()),
           void(),1);

template<typename T>
using has_add_sub =
  decltype((std::declval<T>()+std::declval<T>()),
           (std::declval<T>()-std::declval<T>()),
           void(),1);

template<
  typename T,
  has_add<T> =1> 
bool test_add2(int)
{ return true; }

template<typename>
bool test_add2(...)
{ return false; }

template<
  typename T, 
  has_add_sub<T> =1>
bool test_add_sub2(int)
{ return true; }

template<typename>
bool test_add_sub2(...)
{ return false; }

int main()
{
  std::cout << std::boolalpha;
  std::cout << "test_add<int>(0) " << test_add<int>(0) << '\n';
  std::cout << "test_add<A>(0)   " << test_add<A>(0)   << '\n';
  std::cout << "test_add<B>(0)   " << test_add<B>(0)   << '\n';
  std::cout << "test_add<C>(0)   " << test_add<C>(0)   << '\n';
  std::cout << "test_add_sub<int>(0) " << test_add_sub<int>(0) << '\n';
  std::cout << "test_add_sub<A>(0)   " << test_add_sub<A>(0)   << '\n';
  std::cout << "test_add_sub<B>(0)   " << test_add_sub<B>(0)   << '\n';
  std::cout << "test_add_sub<C>(0)   " << test_add_sub<C>(0)   << '\n';
  std::cout << "test_add2<int>(0) " << test_add2<int>(0) << '\n';
  std::cout << "test_add2<A>(0)   " << test_add2<A>(0)   << '\n';
  std::cout << "test_add2<B>(0)   " << test_add2<B>(0)   << '\n';
  std::cout << "test_add2<C>(0)   " << test_add2<C>(0)   << '\n';
  std::cout << "test_add_sub2<int>(0) " << test_add_sub2<int>(0) << '\n';
  std::cout << "test_add_sub2<A>(0)   " << test_add_sub2<A>(0)   << '\n';
  std::cout << "test_add_sub2<B>(0)   " << test_add_sub2<B>(0)   << '\n';
  std::cout << "test_add_sub2<C>(0)   " << test_add_sub2<C>(0)   << '\n';
  return 0;
}

【讨论】:

  • 好吧,实际上,我比接受的答案更喜欢这个(它在解决问题时仍会被接受)。我在模板中切换到 decltype(expression, nullptr) = nullptr 形式。这很简单,看起来更干净,@Useless 答案会在某些情况下破坏我的代码,我必须为此引入解决方法。
  • 如果我是恶意的(或者只是目光短浅),我会在这个类型中加入operator,(是的逗号)重载。您可以通过将每个1 替换为void(), 1 来使其更难。这将强制使用内置逗号。
  • @StoryTeller-UnslanderMonica 谢谢你的好收获。我进行了相应的编辑。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-06
  • 2017-12-17
  • 1970-01-01
  • 2023-03-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多