put_money 函数的参数是按值传递还是按引用传递?

8
以下代码会引起未定义的行为吗?
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <experimental/iterator>

int main() {
    long double values[] = {1, 2, 3};
    std::transform(
        std::begin(values), std::end(values),
        std::experimental::make_ostream_joiner(std::cout, ", "),
        [](long double v) {
            return std::put_money(v + 1);
        }
    );
    return 0;
}

我的担忧是return std::put_money(v + 1)返回的是对临时变量v + 1的引用。


标准没有规定任何寿命要求,这是一个缺陷吗? - Eric
4
似乎[](const int& i){...}可以解决任何潜在的生命周期问题? - 0x5453
1
我在标准的任何部分中都没有看到指定操纵器返回对象生命周期的内容。我认为这是未经规定的。 - SergeyA
1
@0x5453:确实,那样做可以解决问题,但如果我用 i + 1 替换掉函数体就没用了。 - Eric
@aschepler:嗯,int可以转换为long double。无论如何,已更新以提高清晰度。@0x5453,已更新以使您的观点无效。 - Eric
显示剩余3条评论
3个回答

6

标准文档([ext.manip]/6)仅定义了此特定表达式:

out << put_­money(mon, intl);

未指定在此期间如何存储 mon,它很可能成为悬空引用并导致 UB。

一种“简单”的解决方法是创建自己的类以知道存储该值:

struct money_putter {
    long double value;

    template<class charT, class traits>
    friend std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& os, const money_putter& mon) {
        return os << std::put_money(mon.value);
    }
};


int main() {
    int values[] = {1, 2, 3};
    std::transform(
        std::begin(values), std::end(values),
        std::experimental::make_ostream_joiner(std::cout, ", "),
        [](int i)  {
            return money_putter{i};  // or i + 1
        }
    );
    return 0;
}

我认为你可以用一个名字更好的lambda_putter通过值捕获i来减少这里的模板代码。 - Eric

2
你可以测试它,但这不会告诉你它是否有保证,而且由于put_money的返回类型未指定,你不能假设返回值不持有引用。
无论如何,让我们来测试一下:
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <experimental/iterator>

int main() {
    int i = 42;
    std::cout << std::put_money(i) << "\n";
    auto x = std::put_money(i);
    i = 43;
    std::cout << x;    
    return 0;
}

使用clang进行输出:
42
43

实际上,答案是肯定的。使用clang时,返回值确实保留了一个引用,并且输出与gcc相同。因此,是的,你的代码存在未定义行为。

1

这个答案很好地回答了我的问题,但我想提供一个更通用的解决方案来确保输出到ostream_joiner的对象不会出现悬空引用,它使用一个lambda来捕获这些引用:

#include <type_traits>
#include <ostream>

template<typename F>
class put_invocation_t {
public:
    constexpr put_invocation_t(F const& f) : f(f) {}
    constexpr put_invocation_t(F&& f) : f(std::move(f)) {}
    template<class charT, class traits>
    friend std::basic_ostream<charT, traits>& operator<<(
        std::basic_ostream<charT, traits>& os, put_invocation_t const& pi
    ) {
        return pi.f(os);
    }
private:
    F f;
};

// or use a deduction guide in C++17
template<typename F>
put_invocation_t<std::decay_t<F>> put_invocation(F&& f) {
    return put_invocation_t<std::decay_t<F>>(std::forward<F>(f));
}

作为使用

std::transform(
    std::begin(values), std::end(values),
    std::experimental::make_ostream_joiner(std::cout, ", "),
    [](long double v) {
        return put_invocation([=](auto& os) -> auto& {
            return os << std::put_money(v + 1);
        });
    }
);

这样做还有一个额外的好处,即可以通过在transform中使用类似以下内容来输出多个值:
return put_invocation([=](auto& os) -> auto& {
    return os << "Value is: " << std::put_money(v + 1);
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接