【问题标题】:c++ transform and lambda - replace for loopc++ 转换和 lambda - 替换 for 循环
【发布时间】:2017-07-12 08:02:00
【问题描述】:

我想用 std::transform 替换 for 循环。由于我对算法和 lambda 函数的经验很少,我想知道这是否是正确的方法

原始代码

for (size_t i=0; i < dataPhase.size(); ++i)
{
    dataPhase[i] = fmod(dataPhase[i], pi*1.00001);
}

std::transform 与 lambda

std::transform(dataPhase.begin(), dataPhase.end(), dataPhase.begin(), 
               [](double v){return fmod(v, pi*1.00001); }
);

我需要在这里抓拍吗?

在这种使用索引的情况下,我可以做些什么来替换 for 循环,如下代码所示:

const int halfsize = int(length/2);
for (size_t i=0; i < length; ++i)
{
    axis[i] = int(i) - halfsize;
}

编辑: 我想扩展问题(如果允许)。

在这种情况下是否可以用不同的东西替换 for 循环

for(std::vector<complex<double> >::size_type i = 0; i != data.size(); i++) {
    dataAmplitude[i] = abs(data[i]);
    dataPhase[i]     = arg(data[i]);
}

这里没有修改原始向量,而是将其值用于两个不同的向量。

【问题讨论】:

  • 如果你得到的结果,它可能是正确的。你观察什么?算法适用于迭代器而不是索引。将第二个 sn-p 转换为使用算法是没有意义的,因为在这种情况下,带有索引的循环更具可读性
  • std::transform 也可用于没有明确索引的类型(如链表等)。只要前两个参数满足“输入迭代器”类别,第三个参数满足“输出迭代器”要求。因此,函数中没有(概念)索引。
  • @Matthias Pospiech 在尝试删除所有 for 循环之前,也许您可​​以看看我的评论 #1.... 大多数情况下,这会阻止您使用 OpenMP。

标签: c++ algorithm c++11 lambda


【解决方案1】:

第 1 部分)

这里不需要捕获,因为您只在 lambda 代码中使用参数 (v) 和全局变量 (pi)。

仅当 lambda 必须从当前范围访问变量(即在您的函数中声明)时才需要捕获。您可以按引用 (&) 或按值 (=) 捕获。

这是一个示例,其中需要“通过引用捕获”,因为“结果”是从 lambda 中修改的(但它也捕获了“搜索值”):

size_t count(const std::vector<char>& values, const char searchValue)
{
 size_t result = 0;
 std::for_each(values.begin(), values.end(), [&](const char& v) {
  if (v == searchValue)
   ++result;
 });
 return result;
}

(在现实世界中请使用std::count_if() 甚至std::count()

编译器为每个捕获的 lamda 创建一个未命名的仿函数(请参阅this question)。函数的构造函数接受参数并将其存储为成员变量。因此,“按值捕获”始终使用元素在定义 lambda 时的值。

下面是编译器可以为我们之前创建的 lambda 生成的代码示例:

class UnnamedLambda
{
public:
 UnnamedLambda(size_t& result_, const char& searchValue_)
  : result(result_), searchValue (searchValue_)
 {}

 void operator()(const char& v)
 {
  // here is the code from the lambda expression
  if (v == searchValue)
   ++result;
 }

private:
 size_t& result;
 const char& searchValue;
};

我们的函数可以重写为:

size_t count(const std::vector<char>& values, const char searchValue)
{
 size_t result = 0;
 UnnamedLambda unnamedLambda(result, searchValue);
 for(auto it = values.begin(); it != values.end(); ++it)
  unnamedLambda(*it);
 return result;
}

第 2 部分)

如果您需要索引,请继续使用 for 循环。

std::transform 允许处理单个元素,因此不提供索引。还有一些其他算法,例如 std::accumulate 可以处理中间结果,但我不知道任何提供索引的算法。

【讨论】:

    【解决方案2】:

    您的所有示例都可以转换为 std::transform 的使用,并使用一些额外的对象来做腿部工作(我在这里使用 boost,因为它是大多数所需类的现有技术)

    // 1
    for (size_t i=0; i < dataPhase.size(); ++i)
    {
        dataPhase[i] = fmod(dataPhase[i], pi*1.00001);
    }
    
    // 2
    const int halfsize = int(length/2);
    for (size_t i=0; i < length; ++i)
    {
        axis[i] = int(i) - halfsize;
    }
    
    // 3
    for(std::vector<complex<double> >::size_type i = 0; i != data.size(); i++) {
        dataAmplitude[i] = abs(data[i]);
        dataPhase[i]     = arg(data[i]);
    }
    

    正如你正确注意到的,1 变成了

    std::transform(dataPhase.begin(), dataPhase.end(), dataPhase.begin(), 
        [](double v){return fmod(v, pi*1.00001); } );
    

    2需要一个数字序列,所以我用boost::integer_range

    const int halfsize = int(length/2);
     // provides indexes 0, 1, ... , length - 1
    boost::integer_range<int> interval = boost::irange(0, length);
    std::transform(interval.begin(), interval.end(), axis.begin(), 
        [halfsize](int i) {return i - halfsize;}); 
    

    3 涉及一对(2 元组)输出,所以我使用boost::zip_iterator 作为目标

    std::transform(data.begin(), data.end(), 
        // turns a pair of iterators into an iterator of pairs
        boost::make_zip_iterator(dataAmplitude.begin(), dataPhase.begin()), 
        [](complex<double> d) { return boost::make_tuple(abs(d), arg(d)); });
    

    【讨论】:

    • 好的。我对替换 for 循环感兴趣有两个原因:非常高的数组的可读性和速度(以及未来对标准算法的多线程支持)。 1) 看起来是一个合理的替代品,但是 2 尤其是 3) 缺乏可读性并引入了不必要的复杂性。因此非常有趣,但我没有考虑实施。
    • 对于2,你可以用boost::irange(-halfsize, halfsize)std::copy代替,但我用std::transform表达了这些
    • @MatthiasPospiech 我不确定你是如何决定“不必要的复杂性”的,除了“引入第 3 方库”。可读性是主观的,你可以习惯“功能”风格
    【解决方案3】:

    这里有一些lambda captures的例子:

    [] 不捕获任何内容

    [&] 通过引用捕获任何被引用的变量

    [=] 通过复制来捕获任何引用的变量

    [=, &foo] 通过复制来捕获任何引用的变量,但是 通过引用捕获变量 foo

    [bar] 通过复制捕获bar;不要复制任何东西

    [this] 捕获封闭类的this指针

    因此,如果您的示例中的 pilocal 变量(不是宏或全局变量),您不能让 [] 而是使用 [pi] 来捕获复制(这对于 double 来说是正常的):

    std::transform(dataPhase.begin(), dataPhase.end(), dataPhase.begin(), 
               [pi](double v){return fmod(v, pi*1.00001); }
    );
    

    对于您的第二个示例,没有内置的 std::transform 提供索引。我认为更好的解决方案是保留你的 for 循环。

    如果你真的想使用 lambda(作为练习),你可以使用:

    const int halfsize = int(length/2);
    
    auto lambda=[&axis,halfsize](const int i){axis[i] = i - halfsize;};
    
    for (size_t i=0; i < length; ++i)
    {
        lambda(i);
    }
    

    const int halfsize = int(length/2);
    
    auto lambda=[halfsize](const int i){return i - halfsize;};  
    
    for (size_t i=0; i < length; ++i)
    {
       axis[i] = lambda(i);
    }
    

    这仅取决于您希望如何设计代码。

    备注 1: 似乎您希望避免“基本”for 循环,但它们并不是必要的邪恶,特别是如果您想使用 OpenMP 获得一些性能(simd 或多线程) .比如

     #pragma omp simd
     for(auto& v_i :v) {   // or worst std::transform
        v_i = fmod(v_i, pi*1.00001);
     }
    

    不支持且不会编译。

    然而

     #pragma omp simd
     for (size_t i=0; i < dataPhase.size(); ++i)
     {
         dataPhase[i] = fmod(dataPhase[i], pi*1.00001);
     }    
    

    可以使用 g++ -fopenmp ... 如果可以使用 simd 则具有潜在的性能增益。关于多线程,可以说即将支持 STL 算法的并行执行,但这仅适用于 C++17。

    备注 2: 不是在 C++ 中,而是在 D language 中,您有一个 foreach 指令,可以让您选择性地包含索引:

    foreach (e; [4, 5, 6]) { writeln(e); }
    // 4 5 6
    

    但是

    foreach (i, e; [4, 5, 6]) { writeln(i, ":", e); }
    // 0:4 1:5 2:6
    

    【讨论】:

    • 我不明白为什么要捕获 pi。它是一个常数。我们使用过但没有退回。而且由于它是一个常数,甚至不能被修改。如果我想修改 pi,捕获它是有意义的。
    • 如果你不使用 [pi] 你会得到一个编译器错误(对于 gcc,类似“错误:'pi' 没有被捕获)。即使没有修改这个 [pi] 定义了” context" 为你的 lambda。例如,如果你想将你的 lambda 复制到另一个地方,你还必须复制 pi。这个上下文是执行 closure 操作所必需的。
    • 如果 pi 是全局常量(在函数 ar 类之外声明),则不必捕获它。 (捕获)lambda 就像一个方法调用。仅当此方法必须访问任何 local 变量时,您必须捕获它们。非捕获 lambda 的工作方式类似于全局函数,甚至可以用于需要全局函数的地方(在当前 C++ 中)。
    • @AndreasH。你是对的,我假设了一个 local 变量。谢谢你的澄清。