【问题标题】:Are there any tricks to use std::cin to initialize a const variable?使用 std::cin 初始化 const 变量有什么技巧吗?
【发布时间】:2012-08-30 01:32:53
【问题描述】:

常见的 std::cin 用法

int X;
cin >> X;

这样做的主要缺点是 X 不能是const。它可以很容易地引入错误;我正在寻找一些技巧来创建一个 const 值,并且只写入一次。

天真的解决方案

// Naive
int X_temp;
cin >> X_temp;
const int X = X_temp;

显然你可以通过将 X 更改为 const& 来改进它;仍然可以修改原始变量。

我正在寻找一个简短而聪明的解决方案来解决这个问题。我确信我不是唯一一个会从这个问题的良好答案中受益的人。

// 编辑: 我希望解决方案能够轻松扩展到其他类型(比如说,所有 POD、std::string 和具有简单构造函数的可移动可复制类)(如果它没有意义,请在 cmets 中告诉我)。

【问题讨论】:

  • 将前两行设为函数返回int :) const int X = read_cin();
  • const int 更改为const int& 不会有任何改进。第二个几乎与const int * const 完全相同,因此您将复制sizeof(int*),而在const int 中,您将复制sizeof(int),因此数据量可能完全相同。使用对 int 的引用毫无意义 - 您可能不应该使用对任何 POD-s 的引用。
  • 我个人喜欢你提出的“天真的解决方案”。您从用户那里读入的值显然不是一个常量,那么您明确地将它的值复制到另一个您承诺不会通过将其标记为 const 来更改的值。它有点难看,但似乎完全符合正在发生的事情。
  • 您的“幼稚”解决方案是应该如何完成的。没有人会因为执行如此简单、平凡的任务而从庞大的类接口中受益。如果你发现自己编写冗长而复杂的函数只是为了设置一个变量,这表明程序设计中出现了严重错误。
  • @BartekBanachewicz “这需要我想阅读的每种类型的函数。”将其设为read_cin<int>() 然后:)

标签: c++ c++11 iostream


【解决方案1】:

我可能会选择返回optional,因为流式传输可能会失败。要测试它是否存在(如果您想分配另一个值),请使用get_value_or(default),如示例所示。

template<class T, class Stream>
boost::optional<T> stream_get(Stream& s){
  T x;
  if(s >> x)
    return std::move(x); // automatic move doesn't happen since
                         // return type is different from T
  return boost::none;
}

Live example.

为了进一步确保当T 不可输入时不会出现重载墙,您可以编写一个特征类来检查stream &gt;&gt; T_lvalue 是否有效以及static_assert 是否有效:

namespace detail{
template<class T, class Stream>
struct is_input_streamable_test{
  template<class U>
  static auto f(U* u, Stream* s = 0) -> decltype((*s >> *u), int());
  template<class>
  static void f(...);

  static constexpr bool value = !std::is_void<decltype(f<T>(0))>::value;
};

template<class T, class Stream>
struct is_input_streamable
  : std::integral_constant<bool, is_input_streamable_test<T, Stream>::value>
{
};

template<class T, class Stream>
bool do_stream(T& v, Stream& s){ return s >> v; }
} // detail::

template<class T, class Stream>
boost::optional<T> stream_get(Stream& s){
  using iis = detail::is_input_streamable<T, Stream>;
  static_assert(iis::value, "T must support 'stream >> value_of_T'");
  T x;
  if(detail::do_stream(x, s))
    return std::move(x); // automatic move doesn't happen since
                         // return type is different from T
  return boost::none;
}

Live example.

我正在使用detail::do_stream 函数,否则s &gt;&gt; x 仍会在get_stream 中进行解析,并且当static_assert 触发时,您仍然会得到我们想要避免的重载墙。将此操作委托给不同的函数即可完成此操作。

【讨论】:

  • 我声称这是唯一可接受的解决方案。但好吧,我很奇怪。
  • @KonradRudolph:这是定义weird 的好方法,就像帖子是overkill 的一个很好的例子一样:-)
  • @Kerrek 构建您的代码流以便拥有const 对象?我不认为这有一点矫枉过正。
  • @KonradRudolph:你用可选的 const 做什么?! “这是一个常数……也许”?
  • @Xeo:这有点开玩笑的意思。我很欣赏解决方案的技巧。我只是对初始化X 的看似简单的愿望有多大的解决方案感到好笑:-)
【解决方案2】:

在这种情况下,您可以使用 lambda:

   const int x = []() -> int {
                     int t;
                     std::cin >> t;
                     return t;
                 }();

(注意末尾多出来的())。

与编写单独的函数不同,这样做的好处是在阅读代码时不必在源文件中跳转。

编辑:由于在 cmets 中指出这违反了 DRY 规则,您可以利用 auto5.1.2:4 来减少类型重复:

5.1.2:4 状态:

[...] 如果 lambda 表达式不包含尾随返回类型,则为 如果尾随返回类型表示以下类型:

  • 如果复合语句的形式是

    { attribute-specifier-seq(opt) return expression ; }

    左值到右值转换后返回表达式的类型(4.1), 数组到指针的转换(4.2),函数到指针的转换(4.3);

  • 否则无效。

所以我们可以将代码更改为如下所示:

   const auto x = [] {
                     int t;
                     std::cin >> t;
                     return t;
                  }();

我无法确定这是否更好,因为该类型现在“隐藏”在 lambda 主体中...

编辑 2: 在 cmets 中指出,仅在可能的情况下删除类型名称不会产生“DRY-correct”代码。 此外,这种情况下的尾随返回类型推导目前实际上是 MSVC++ 以及 g++ 的扩展,而不是(还)标准。

【讨论】:

  • 嗯,你能跳过[]之后的第一组()吗?
  • @Bartek:如果您想提供尾随返回类型,则不需要。
  • @BartekBanachewicz 至于 DRY:您可以使用 auto,但这只会为您节省一次重复。
  • @lx。它在 5.1.2:1 的语法中; lambda-declarator 是可选的,但如果包含它,它必须在可选的trailing-return-type 之前包含一个parameter-declaration-clause。省略的 lambda-declarator 或省略的 trailing-return-type 的语义在 5.1.2:4 中定义。
  • DRY 的真正精神是将模式重构为一个函数,以便最终得到例如const auto i = extract&lt;int&gt;(std::cin);,并不是说你让模式变短了。
【解决方案3】:

对 lx. 的 lambda 解决方案稍作调整:

const int x = [](int t){ return iss >> t, t; }({});

显着减少 DRY 违规;可以通过将const int x 更改为const auto x 来完全消除:

const auto x = [](int t){ return iss >> t, t; }({});

进一步改进;您可以将副本转换为移动,否则逗号运算符会抑制 12.8:31 (Move constructor suppressed by comma operator) 中的优化:

const auto x = [](int t){ return iss >> t, std::move(t); }({});

请注意,这仍然可能比 lx. 的 lambda 效率低,因为它可以从 NRVO 中受益,而这仍然必须使用移动构造函数。另一方面,优化编译器应该能够优化出无副作用的移动。

【讨论】:

  • 我真的很喜欢你的最后一个例子
【解决方案4】:

您可以调用函数返回结果并在同一语句中进行初始化:

template<typename T>
const T in_get (istream &in = std::cin) {
    T x;
    if (!(in >> x)) throw "Invalid input";
    return x;
}

const int X = in_get<int>();
const string str = in_get<string>();

fstream fin("myinput.in",fstream::in);
const int Y = in_get<int>(fin);

示例:http://ideone.com/kFBpT

如果您有 C++11,那么如果您使用 auto&amp;&amp; 关键字,则只能指定一次类型。

auto&& X = in_get<int>();

【讨论】:

  • @KerrekSB 你能添加默认值参数来消除歧义吗?喜欢in_get(T temp = T())
  • 好的,把问题解决了,链接到ideone.com,证明有效。
  • 我删除了我之前的评论,因为我意识到提供“temp”变量实际上意味着编写一个类型。
  • 你可以使用auto而不是重新指定类型。
  • 为什么是auto&amp;&amp; 而不是const auto
【解决方案5】:

当然,您只需构建一个临时的istream_iterator 就可以做到这一点。例如:

const auto X = *istream_iterator<int>(cin)

在此值得指出的是,当您执行此操作时,您将放弃对错误检查的所有希望。通常从用户那里获取输入不会被认为是最明智的......但是,嘿,也许你已经以某种方式策划了这个输入?

Live Example

【讨论】:

  • 5 年来没有人想到这一点,真是太神奇了!
  • @BartekBanachewicz 我只能猜测这主要与缺乏错误检查有关?我经常使用我可以控制的istringstreams 和ifstreams。但是...是的,我不知道这似乎是最明显的答案?
【解决方案6】:

我假设你想要初始化一个 global 变量,因为对于一个局部变量来说,放弃三行简单易懂的语句来获得一个有问题的常数。

在全局范围内,我们不能在初始化中出现错误,因此我们必须以某种方式处理它们。这里有一些想法。

首先,一个模板化的小构造助手:

template <typename T>
T cinitialize(std::istream & is) noexcept
{
    T x;
    return (is && is >> x) ? x : T();
}

int const X = cinitialize<int>(std::cin);

请注意,全局初始化程序不得抛出异常(在std::terminate 的痛苦下),并且输入操作可能会失败。总而言之,以这种方式从用户输入初始化全局变量可能是非常糟糕的设计。可能会显示一个致命错误:

template <typename T>
T cinitialize(std::istream & is) noexcept
{
    T x;

    if (!(is && is >> x))
    {
        std::cerr << "Fatal error while initializing constants from user input.\n";
        std::exit(1);
    }

    return x;
}

只是为了在 cmets 中进行一些讨论后澄清我的立场:在本地范围内,我永远不会求助于如此笨拙的拐杖。由于我们正在处理用户提供的外部数据,因此作为正常控制流程的一部分,我们基本上不得不忍受失败:

void foo()
{
    int x;

    if (!(std::cin >> x)) { /* deal with it */ }
}

我让你来决定是写太多还是太难读。

【讨论】:

  • @AntonGolov:从全局初始化程序中抛出异常会终止程序。
  • @KerrekSB:全局初始化器允许抛出异常(对于“允许”的某些值 - 它会导致调用 std::terminate);并且没有迹象表明 OP 想要以这种方式初始化全局变量。
  • 顺便说一句,我认为您不需要仔细检查流本身是否有效,这将在 operator&gt;&gt; 内完成。
  • 停止有关异常的 FUD。在动态初始化期间抛出异常将不会导致任何不好的事情发生。 让异常逃逸会。因此,没有必要让每个调用者都承担使用这种低于标准的错误报告方法的负担。使用异常,如果有人想在 SSDO 初始化程序中使用该函数 他们 会注意例如将调用包装在 lambda 中的 try-catch 块中。我知道你总比无理地害怕例外要好。
  • OP 没有明确说这是关于初始化命名空间范围变量。无论如何,未来的访问者可能会从您的答案中获得表面价值,并且确实认为在 SSDO 初始化程序中使用异常是难以处理的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-01
  • 2019-04-15
  • 1970-01-01
  • 2012-01-16
  • 2010-09-11
  • 1970-01-01
相关资源
最近更新 更多