对于基于SFINAE的特性,难以理解通用lambda的语法。

huangapple go评论68阅读模式
英文:

Have difficulty understanding the syntax of generic lambdas for SFINAE-based traits

问题

  1. 对于第一个问题,std::declval<F>()(std::declval<Args&&>()...) 表示一个类型为 F 的可调用对象(functor),它接受类型为 Args 的参数。为什么要使用 forwarding reference (Args&&) 而不是简单的 Args?这是因为这里使用的是 SFINAE(Substitution Failure Is Not An Error)技术,它允许在编译时根据模板参数的类型进行条件编译。使用 forwarding reference 可以确保参数传递方式被保留,从而使编译器可以正确地匹配和推断参数类型。

  2. 对于第二个问题,decltype(isValidImpl<decltype(f), decltype(args)&&...>(nullptr)) 也是使用了 SFINAE 技术。这里之所以传递 decltype(args)&& 而不是 decltype(args),是因为 forwarding reference 保留了参数的引用性质,这对于正确匹配和推断参数类型非常重要。编写通用的 SFINAE 代码时,通常需要保留参数的引用性质以便进行正确的类型检查。

  3. 对于第四个问题,decltype((void)decltype(valueT(x))()) 中的 (void) 强制类型转换的目的是消除表达式的返回值,使其成为一个表达式语句,同时抑制编译器对于返回值的不必要警告。这种技巧常常用于 SFINAE 上下文中,因为 SFINAE 依赖于函数重载和函数模板匹配,而不是函数的返回值。 (void) 强制类型转换确保了编译器不会把这个表达式的返回值视为重要,而只关注函数签名是否有效。这是一种常见的做法,用于确保 SFINAE 行为的正确性。

希望这些解释能够帮助你更好地理解这些复杂的语法和技巧。如果你还有其他问题,请随时提出。

英文:

I am reading some examples of SFINAE-based traits, but unable to make sense out of the one related to generic lambdas in C++17 (isvalid.hpp).

I can understand that it roughly contains some major parts in order to implement a type trait such as isDefaultConstructible or hasFirst trait (isvalid1.cpp):

1. Helper functions using SFINAE technique:

#include &lt;type_traits&gt;

// helper: checking validity of f(args...) for F f and Args... args:
template&lt;typename F, typename... Args,
         typename = decltype(std::declval&lt;F&gt;()(std::declval&lt;Args&amp;&amp;&gt;()...))&gt;
std::true_type isValidImpl(void*);

// fallback if helper SFINAE&#39;d out:
template&lt;typename F, typename... Args&gt;
std::false_type isValidImpl(...);

2. Generic lambda to determine the validity:

// define a lambda that takes a lambda f and returns whether calling f with args is valid
inline constexpr
auto isValid = [](auto f) {
                 return [](auto&amp;&amp;... args) {
                          return decltype(isValidImpl&lt;decltype(f),
                                                      decltype(args)&amp;&amp;...
                                                     &gt;(nullptr)){};
                        };
               };

3. Type helper template:

// helper template to represent a type as a value
template&lt;typename T&gt;
struct TypeT {
    using Type = T;
};

// helper to wrap a type as a value
template&lt;typename T&gt;
constexpr auto type = TypeT&lt;T&gt;{};

// helper to unwrap a wrapped type in unevaluated contexts
template&lt;typename T&gt;
T valueT(TypeT&lt;T&gt;);  // no definition needed

4. Finally, compose them into isDefaultConstructible trait to check whether a type is default constructible:

constexpr auto isDefaultConstructible
	= isValid([](auto x) -&gt; decltype((void)decltype(valueT(x))()) {
		});

It is used like this (Live Demo):

struct S {
    S() = delete;
};

int main() {
    std::cout &lt;&lt; std::boolalpha;
    std::cout &lt;&lt; &quot;int: &quot; &lt;&lt; isDefaultConstructible(type&lt;int&gt;) &lt;&lt; std::endl;    // true
    std::cout &lt;&lt; &quot;int&amp;: &quot; &lt;&lt; isDefaultConstructible(type&lt;int&amp;&gt;) &lt;&lt; std::endl;  // false
    std::cout &lt;&lt; &quot;S: &quot; &lt;&lt; isDefaultConstructible(type&lt;S&gt;) &lt;&lt; std::endl;        // false

    return 0;
}

However, some of the syntax are so complicated and I cannot figure out.

My questions are:

  • With respect to 1, as for std::declval&lt;F&gt;()(std::declval&lt;Args&amp;&amp;&gt;()...), does it mean that it is an F type functor taking Args type constructor? And why it uses forwarding reference Args&amp;&amp; instead of simply Args?

  • With respect to 2, as for decltype(isValidImpl&lt;decltype(f), decltype(args)&amp;&amp;...&gt;(nullptr)){} , I also cannot understand why it passes forwarding reference decltype(args)&amp;&amp; instead of simply decltype(args)?

  • With respect to 4, as for decltype((void)decltype(valueT(x))()), what is the purpose of (void) casting here? ((void) casting can also be found in isvalid1.cpp for hasFirst trait) All I can find about void casting is https://stackoverflow.com/questions/69314599/casting-to-void-to-avoid-use-of-overloaded-user-defined-comma-operator, but it seems it is not the case here.

Thanks for any insights.


P.S. For one who wants more detail could check C++ Templates: The Complete Guide, 2nd - 19.4.3 Using Generic Lambdas for SFINAE. The author also mentioned that some of the techniques are used widely in Boost.Hana, so I also listen to Louis Dionne's talk about it. Yet, it only helps me a little to understand the code snippet above. (It is still a great talk about the evolution of C++ metaprogramming)

答案1

得分: 1

  1. F 是一个可调用的函数对象,带有参数 Args...
    为了理解起来更清晰,将 std::declval&lt;F&gt;() 想象成一个 "类型为 F 的完全构造的对象"。std::declval 只是为了防止 F 不可默认构造并仍需在未评估的上下文中使用。
    对于可默认构造的类型,等效的写法是:
    F()(std::declval&lt;Args&amp;&amp;&gt;()...); 本质上是对 F 的构造函数的调用,然后使用转发的 Args 调用其 operator()。但想象一种类型可用 int 构造,另一种类型可默认构造,还有一种类型需要一个字符串。如果没有一些未评估的构造函数类似的元函数,将无法覆盖所有这些情况。
    您可以在 Alexandrescu 的 Modern C++ Design: Generic Programming and Design Patterns Applied 中阅读更多相关内容。

  2. 向参数类型添加 &amp;&amp; 等效于完美转发它。这可能看起来有些晦涩,但它只是 decltype(std::forward&lt;decltype(args)&gt;(args)) 的一种缩写。更多细节,请参阅 std::forward 的实现引用折叠规则。请注意,此片段添加的是右值引用,当与原始类型组合时会折叠为正确的引用,而不是转发引用。

  3. 正如评论中所述:实际上不需要该类型,有可能无法返回它,它的存在只是为了检查表达式的正确性,之后可以丢弃它。

英文:
  1. F is a function object callable with Args...
    For the sake of mental model, picture std::declval&lt;F&gt;() as a "fully constructed object of type F". std::declval is there just in case F is not default-constructible and still needs to be used in unevaluated contexts.
    For a default-constructible type this would be equivalent:
    F()(std::declval&lt;Args&amp;&amp;&gt;()...); In essence it's a call to F's constructor and then call to its operator() with forwarded Args. But imagine one type is constructible with int, another one is default-constructible, yet another one requires a string. Without some unevaluated constructor-like metafunction it would be impossible to cover all those cases.
    You can read more on that in Alexandrescu's Modern C++ Design: Generic Programming and Design Patterns Applied.

  2. Adding &amp;&amp; to the argument type is effectively perfect-forwarding it. It may look obscure, but it's just a shorthand for decltype(std::forward&lt;decltype(args)&gt;(args)). See the implementation of std::forward and reference collapsing rules for more details. Keep in mind though, that this snippet adds rvalue-reference that collapses to the correct one when combined with the original type, not a forwarding one.

  3. As it was stated in the comments: the type is not really needed, possibilty exists it cannot be returned, its presence there is just to check expression's correctness, afterwards it can be discarded.

huangapple
  • 本文由 发表于 2023年6月1日 22:09:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/76382810.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定