【问题标题】:void_t "can implement concepts"?void_t“可以实现概念”?
【发布时间】:2014-10-22 17:07:01
【问题描述】:

我在看Walter Brown's CppCon2014 talk on template metaprogramming的第二部分,期间他讨论了他的小说void_t<>构造的用途。在他的演讲中,Peter Sommerlad 问了他一个我不太明白的问题。 (链接直接指向问题,正在讨论的代码直接发生在此之前)

萨默拉德问

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

沃尔特的回应

哦,是的!我已经完成了...它的语法不太一样。

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

void_t模板定义如下:

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

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

//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 这样的东西。

【问题讨论】:

  • 有趣的问题,不幸的是,它与“请看一下我的源代码,托管在(某个存储库)上”的种类并没有什么不同。更糟糕的是,因为它是一个视频。您能否将相关代码 sn-ps 添加到您的问题中,使其变得更加独立?至少,指定您正在谈论的视频中的时间,“第二部分”几乎没用。
  • @BenVoigt 好的,我更新了问题。 youtube 链接中有一个时间码,可以直接跳到问题。我添加了他在演示文稿中使用的主要示例。我希望这能让问题更清楚。
  • 您无法从那里获得概念精简版。这是一种很酷的技术,您可以将它用于模板参数列表中的“requires”子句,但是您不能让编译器根据哪个“概念”更精细来对模板进行部分排序。您也不能对类模板的非模板成员函数或属于概念 TS 一部分的其他几个全新的语言特性强加要求。
  • @JonathanWakely 好吧,你说的这些“要求”条款(我们的假的)只是用更易于访问和易于编写的约束和类型特征修饰enable_if,还是更复杂?
  • 是的,我相信这是一种非常酷的 SFINAE 方式。 Concepts Lite 远不止 SFINAE,它完全替换 SFINAE,而不是用漂亮的语法修饰它。

标签: c++ templates c++11 template-meta-programming c++-concepts


【解决方案1】:

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

定义谓词

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

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

所以如果++x 无效,则requires_ 成员函数不可调用。所以我们可以创建一个models trait 来检查requires_ 是否可以使用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,但是我们可以使用这个宏来帮助使其更简洁:

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

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

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

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

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,我们可以这样定义它(现在我们将忽略可递减的情况):

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 迭代器一起使用时,这会导致模棱两可的重载(在概念 lite 中,这仍然是模棱两可的重载,除非我们将谓词更改为引用 concept bool 中的其他谓词)。我们要做的是对调用进行排序,我们可以使用条件重载来做到这一点。可以考虑写这样的东西(这不是有效的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 的类,它接受两个函数对象作为模板参数:

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)...);
    }
};

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

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 一起使用:

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,它适用于使用递归的任意数量的函数:

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

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

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

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宏来构造lambda:

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 函数:

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;
    }
);

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

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

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 库可以处理函数和重载。

【讨论】:

  • 感谢您抽出宝贵时间撰写如此详尽的答案。这超出了我的预期,但正是我所希望的那种答案。
  • 由于高质量和数量多而受到支持......但天哪,我的 C++11 恶心开始了。
  • FWIW,用于实现 STATIC_LAMBDA 的技巧是符合标准的 C++。它在实践中有效,因为系统必须非常奇怪才能使其无法工作,但这并不能使其 100% 符合犹太教规。
  • @mattnewport 是的,它匹配一个函数类型,其中Ts...匹配函数参数,Concept匹配函数类型的返回类型。
  • 啊,好吧,我想我现在明白了。让我失望的是没有看到具有该签名的函数来自哪里,但实际上从来没有这样的函数,它只是一种方便的语法来模式匹配类型。聪明。
猜你喜欢
  • 2014-07-15
  • 2013-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-14
  • 2012-06-20
  • 1970-01-01
相关资源
最近更新 更多