void_t“可以实现概念"?

我正在观看 Walter Brown 的 CppCon2014 的第二部分谈论模板元编程,在此期间,他讨论了他新颖的void_t<> 构造的使用.在他的演讲中,Peter Sommerlad 问了他一个我不太明白的问题.(链接直接指向问题,讨论中的代码直接发生在此之前)

I was watching the second part of Walter Brown's CppCon2014 talk on template metaprogramming, during which he discussed the uses of his novel void_t<> construction. During his presentation Peter Sommerlad asked him a question that I didn't quite understand. (link goes directly to the question, the code under discussion took place directly before that)

萨默拉德问道

Walter,这是否意味着我们现在实际上可以实现精简版的概念?

Walter, would that mean we actually can implement concepts lite right now?

沃尔特回应了

哦耶!我已经完成了……它的语法不太一样.

Oh yeah! I've done it ... It doesn't have quite the same syntax.

我理解这次交流是关于 Concepts Lite.这种模式真的那个通用吗?无论出于何种原因,我都没有看到它.有人可以解释(或草图)这样的东西会是什么样子吗?这只是关于 enable_if 和定义特征,还是提问者指的是什么?

I understood this exchange to be about Concepts Lite. Is this pattern really that versatile? For whatever reason, I am not seeing it. Can someone explain (or sketch) how something like this might look? Is this just about enable_if and defining traits, or what was the questioner referring to?

void_t 模板定义如下:

template<class ...> using void_t = void;

他使用 then 来检测类型语句是否格式正确,并使用它来实现 is_copy_assignable 类型特征:

He uses this then to detect if type statements are well formed, using this to implement the is_copy_assignable type trait:

//helper type
template<class T>
using copy_assignment_t
= decltype(declval<T&>() = declval<T const&>());

//base case template
template<class T, class=void>
struct is_copy_assignable : std::false_type {};

//SFINAE version only for types where copy_assignment_t<T> is well-formed.
template<class T>
struct is_copy_assignable<T, void_t<copy_assignment_t<T>>> 
: std::is_same<copy_assignment_t<T>,T&> {};

因为谈话,我明白这个例子是如何工作的,但我不明白我们如何从这里得到像 Concepts Lite 这样的东西.

Because of the talk, I understand how this example works, but I don't see how we get from here to something like Concepts Lite.

推荐答案

是的,concepts lite 基本上装扮了 SFINAE.此外,它还允许进行更深入的内省,以实现更好的重载.然而,这仅在概念谓词被定义为 concept bool 时才有效.改进的重载不适用于当前的概念谓词,但可以使用条件重载.让我们看看如何在 C++14 中定义谓词、约束模板和重载函数.这有点长,但它介绍了如何在 C++14 中创建完成此任务所需的所有工具.

Yes, concepts lite basically dresses up SFINAE. Plus it allows deeper introspection to allow for better overloading. However that only works if the concept predicates are defined as concept bool. The improved overloading does not work with the current concept predicates, but conditional overloading can be used. Lets look how we can define predicates, constrain templates, and overload functions in C++14. This is kind of long, but it goes over how to create all of the tools needed to accomplish this in C++14.

首先,阅读带有所有 std::declvaldecltype 的谓词有点难看.相反,我们可以利用这样一个事实,即我们可以使用尾随 decltype 来约束函数(来自 Eric Niebler 的博客文章 这里),像这样:

First, it is kind of ugly to read the predicate with all the std::declval and decltype everywhere. Instead, we can take advantage of the fact that we can constrain a function using a trailing decltype(from Eric Niebler’s blog post here), like this:

struct Incrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(++x);
};

因此,如果 ++x 无效,则 requires_ 成员函数不可调用.所以我们可以创建一个 models trait 来检查 requires_ 是否可以使用 void_t 调用:

So if ++x is not valid, then the requires_ member function is not callable. So we can create a models trait that just checks if requires_ is callable using void_t:

template<class Concept, class Enable=void>
struct models
: std::false_type
{};

template<class Concept, class... Ts>
struct models<Concept(Ts...), void_t< 
    decltype(std::declval<Concept>().requires_(std::declval<Ts>()...))
>>
: std::true_type
{};

约束模板

所以当我们想基于概念约束模板时,我们仍然需要使用enable_if,但我们可以使用这个宏来帮助使其更清晰:

Constraining Templates

So when we want to constrain the template based on the concept, we will still need to use enable_if, but we can use this macro to help make it cleaner:

#define REQUIRES(...) typename std::enable_if<(__VA_ARGS__), int>::type = 0

所以我们可以定义一个基于Incrementable概念约束的increment函数:

So we can define an increment function that is constrained based on Incrementable concept:

template<class T, REQUIRES(models<Incrementable(T)>())>
void increment(T& x)
{
    ++x;
}

所以如果我们用不是Incrementable的东西调用increment,我们会得到这样的错误:

So if we call increment with something that is not Incrementable, we will get an error like this:

test.cpp:23:5: error: no matching function for call to 'incrementable'
    incrementable(f);
    ^~~~~~~~~~~~~
test.cpp:11:19: note: candidate template ignored: disabled by 'enable_if' [with T = foo]
template<class T, REQUIRES(models<Incrementable(T)>())>
                  ^

重载函数

现在如果我们要做重载,我们要使用条件重载.假设我们要创建一个 std::advance使用概念谓词,我们可以这样定义它(现在我们将忽略可递减的情况):

Overloading Functions

Now if we want to do overloading, we want to use conditional overloading. Say we want to create an std::advance using concept predicates, we could define it like this(for now we will ignore the decrementable case):

struct Incrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(++x);
};

struct Advanceable
{
    template<class T, class I>
    auto requires_(T&& x, I&& i) -> decltype(x += i);
};

template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
void advance(Iterator& it, int n)
{
    it += n;
}

template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
void advance(Iterator& it, int n)
{
    while (n--) ++it;
}

然而,当它与 std::vector 迭代器.我们想要做的是对调用进行排序,我们可以使用条件重载来完成.可以考虑写这样的东西(这不是有效的 C++):

However, this causes an ambiguous overload(In concepts lite this would still be an ambiguous overload unless we change our predicates to refer to the other predicates in a concept bool) when its used with std::vector iterator. What we want to do is order the calls, which we can do using conditional overloading. It can be thought of writing something like this(which is not valid C++):

template<class Iterator>
void advance(Iterator& it, int n) if (models<Advanceable(Iterator, int)>())
{
    it += n;
} 
else if (models<Incrementable(Iterator)>())
{
    while (n--) ++it;
}

所以如果第一个函数没有被调用,它将调用下一个函数.因此,让我们从为两个功能实现它开始.我们将创建一个名为 basic_conditional 的类,它接受两个函数对象作为模板参数:

So if the first function isn't called, it will call the next function. So lets start by implementing it for two functions. We will create a class called basic_conditional which accepts two function objects as template parameters:

struct Callable
{
    template<class F, class... Ts>
    auto requires_(F&& f, Ts&&... xs) -> decltype(
        f(std::forward<Ts>(xs)...)
    );
};

template<class F1, class F2>
struct basic_conditional
{
    // We don't need to use a requires clause here because the trailing
    // `decltype` will constrain the template for us.
    template<class... Ts>
    auto operator()(Ts&&... xs) -> decltype(F1()(std::forward<Ts>(xs)...))
    {
        return F1()(std::forward<Ts>(xs)...);
    }
    // Here we add a requires clause to make this function callable only if
    // `F1` is not callable.
    template<class... Ts, REQUIRES(!models<Callable(F1, Ts&&...)>())>
    auto operator()(Ts&&... xs) -> decltype(F2()(std::forward<Ts>(xs)...))
    {
        return F2()(std::forward<Ts>(xs)...);
    }
};

所以现在这意味着我们需要将我们的函数定义为函数对象:

So now that means we need to define our functions as functions objects instead:

struct advance_advanceable
{
    template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
    void operator()(Iterator& it, int n) const
    {
        it += n;
    }
};

struct advance_incrementable
{
    template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
    void operator()(Iterator& it, int n) const
    {
        while (n--) ++it;
    }
};

static conditional<advance_advanceable, advance_incrementable> advance = {};

所以现在如果我们尝试将它与 std::vector 一起使用:

So now if we try to use it with an std::vector:

std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
auto iterator = v.begin();
advance(iterator, 4);
std::cout << *iterator << std::endl;

它会编译并打印出5.

然而,std::advance 实际上有三个重载,所以我们可以使用 basic_conditional 来实现适用于任意数量的 conditional使用递归的函数:

However, std::advance actually has three overloads, so we can use the basic_conditional to implement conditional that works for any number of functions using recursion:

template<class F, class... Fs>
struct conditional : basic_conditional<F, conditional<Fs...>>
{};

template<class F>
struct conditional<F> : F
{};

所以,现在我们可以像这样编写完整的std::advance:

So, now we can write the full std::advance like this:

struct Incrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(++x);
};

struct Decrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(--x);
};

struct Advanceable
{
    template<class T, class I>
    auto requires_(T&& x, I&& i) -> decltype(x += i);
};

struct advance_advanceable
{
    template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
    void operator()(Iterator& it, int n) const
    {
        it += n;
    }
};

struct advance_decrementable
{
    template<class Iterator, REQUIRES(models<Decrementable(Iterator)>())>
    void operator()(Iterator& it, int n) const
    {
        if (n > 0) while (n--) ++it;
        else 
        {
            n *= -1;
            while (n--) --it;
        }
    }
};

struct advance_incrementable
{
    template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
    void operator()(Iterator& it, int n) const
    {
        while (n--) ++it;
    }
};

static conditional<advance_advanceable, advance_decrementable, advance_incrementable> advance = {};

Lambda 重载

然而,此外,我们可以使用 lambdas 代替函数对象来编写它,这有助于使其编写起来更清晰.所以我们使用这个 STATIC_LAMBDA 宏来在编译时构造 lambdas:

Overloading With Lambdas

However, additionally, we could use lambdas to write it instead of function objects which can help make it cleaner to write. So we use this STATIC_LAMBDA macro to construct lambdas at compile time:

struct wrapper_factor
{
    template<class F>
    constexpr wrapper<F> operator += (F*)
    {
        return {};
    }
};

struct addr_add
{
    template<class T>
    friend typename std::remove_reference<T>::type *operator+(addr_add, T &&t) 
    {
        return &t;
    }
};

#define STATIC_LAMBDA wrapper_factor() += true ? nullptr : addr_add() + []

并添加一个make_conditional函数,即constexpr:

template<class... Fs>
constexpr conditional<Fs...> make_conditional(Fs...)
{
    return {};
}

那么我们现在可以像这样编写advance函数:

Then we can now write the advance function like this:

constexpr const advance = make_conditional(
    STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Advanceable(decltype(it), int)>()))
    {
        it += n;
    },
    STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Decrementable(decltype(it))>()))
    {
        if (n > 0) while (n--) ++it;
        else 
        {
            n *= -1;
            while (n--) --it;
        }
    },
    STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Incrementable(decltype(it))>()))
    {
        while (n--) ++it;
    }
);

这比使用函数对象版本更紧凑和可读.

Which is little more compact and readable than using the function object versions.

另外,我们可以定义一个modeled函数来减少decltype的丑陋:

Additionally, we could define a modeled function to reduce down the decltype ugliness:

template<class Concept, class... Ts>
constexpr auto modeled(Ts&&...)
{
    return models<Concept(Ts...)>();
}

constexpr const advance = make_conditional(
    STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Advanceable>(it, n)))
    {
        it += n;
    },
    STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Decrementable>(it)))
    {
        if (n > 0) while (n--) ++it;
        else 
        {
            n *= -1;
            while (n--) --it;
        }
    },
    STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Incrementable>(it)))
    {
        while (n--) ++it;
    }
);

最后,如果您有兴趣使用现有的库解决方案(而不是像我展示的那样滚动自己的解决方案).Tick 库提供了一个定义概念和约束模板的框架.而 Fit 库可以处理函数和重载.

Finally, if you are interested in using existing library solutions(rather than rolling your own like I've shown). There is the Tick library that provides a framework for defining concepts and constraining templates. And the Fit library can handle the functions and overloading.

相关文章