如何干净地使用:const char* 和 std::string?

How to cleanly use: const char* and std::string?

tl:dr

How can I concatenate const char* with std::string, neatly and elegantly, without multiple function calls. Ideally in one function call and have the output be a const char*. Is this impossible, what is an optimum solution?

初始问题

到目前为止,我在使用 C++ 时遇到的最大障碍是它如何处理字符串。在我看来,在所有广泛使用的语言中,它对字符串的处理最差。我见过其他与此类似的问题,要么有一个答案说 "use std::string",要么只是指出其中一个选项最适合您的情况。

然而,当尝试像在其他语言中那样动态使用字符串时,这是无用的建议。我不能保证总是能够使用 std::string 并且在我必须使用 const char* 的时候我撞到了明显的 "it's constant, you can't concatenate it" 墙。

我在 C++ 中看到的任何字符串操作问题的每个解决方案都需要重复的多行代码,这些代码只适用于该字符串格式。 我希望能够将任何一组字符与 + 符号连接起来,或者像我在 C# 或 Python 中那样使用简单的 format() 函数。为什么没有简单的选项?

现状

标准输出

我正在编写一个 DLL,到目前为止,我已经通过 << 运算符将文本输出到 cout。到目前为止,使用以下形式的简单字符数组一切正常:

cout << "Hello world!"

运行时字符串

现在到了我想在运行时构造一个字符串并用 class 存储它的地步,这个 class 将保存一个报告一些错误的字符串,以便他们可以被其他 classes 拾取并可能稍后发送到 cout,该字符串将由函数 SetReport(const char* report) 设置。所以我真的不想为此使用多行,所以我继续写类似的东西:

SetReport("Failure in " + __FUNCTION__ + ": foobar was " + foobar + "\n"); // __FUNCTION__ gets the name of the current function, foobar is some variable

我当然会立即得到:

丑陋的字符串

没错。所以我试图将两个或更多 const char* 加在一起,但这不是一个选项。所以我发现这里的主要建议是使用 std::string,有点奇怪,输入 "Hello world!" 不仅首先给你一个,但让我们试一试:

SetReport(std::string("Failure in ") + std::string(__FUNCTION__) + std::string(": foobar was ") + std::to_string(foobar) + std::string("\n"));

太棒了!有用!但是看看那有多丑!!这是我见过的最丑陋的代码。我们可以简化为:

SetReport(std::string("Failure in ") + __FUNCTION__ + ": foobar was " + std::to_string(foobar) + "\n");

这可能仍然是我遇到过的最糟糕的简单的一行字符串连接方式,但现在一切都应该没问题了吧?

转换回常量

嗯,不,如果你正在处理 DLL,这是我经常做的事情,因为我喜欢单元测试,所以我需要我的 C++ 代码被单元测试库导入,你会发现当您尝试将该报告字符串设置为 class 的成员变量作为 std::string 时,编译器会抛出一条警告:

warning C4251: class 'std::basic_string<_Elem,_Traits,_Alloc>' needs to have dll-interface to be used by clients of class'

除了 "ignore the warning"(糟糕的做法!),我发现这个问题的唯一真正解决方案是使用 const char* 作为成员变量而不是 std::string,但是这并不是真正的解决方案,因为现在您必须将丑陋的串联(但动态)字符串转换回您需要的 const char 数组。但是你不能只在最后标记 .c_str() (尽管你为什么要这样做,因为这个连接在第二个变得更加荒谬?)你必须确保 std::string 不干净建立新构造的字符串并留下垃圾。所以你必须在接收字符串的函数中执行此操作:

const std::string constString = (input);
m_constChar = constString.c_str();

这太疯狂了。因为现在我遍历了几种不同类型的字符串,使我的代码变得丑陋,添加了比应该需要的更多的行,而所有这些只是为了将一些字符粘在一起。为什么这么难?

解决方案?

那么解决方法是什么?我觉得我应该能够制作一个函数,将 const char* 连接在一起,但也可以处理其他对象类型,例如 std::stringintdouble,我强烈认为这应该可以在一行中实现,但我找不到任何实现它的例子。我应该使用 char* 而不是常量变体,即使我读过你永远不应该更改 char* 的值,那么这有什么帮助?

是否有经验丰富的 C++ 程序员解决了这个问题并且现在对 C++ 字符串感到满意,您的解决方案是什么?没有解决办法吗?不可能吗?

构建字符串(将非字符串类型格式化为字符串)的标准方法是 string stream

#include <sstream>

std::ostringstream ss;
ss << "Failure in " << __FUNCTION__ << ": foobar was " << foobar << "\n";
SetReport(ss.str());

如果你经常这样做,你可以编写一个可变参数模板来做到这一点:

template <typename... Ts> std::string str(Ts&&...);
SetReport(str("Failure in ", __FUNCTION__, ": foobar was ", foobar, '\n'));

实施留作 reader 的练习。

在这种特殊情况下,字符串文字(包括__FUNCTION__)可以通过简单地一个接一个地连接起来;并且,假设 foobar 是一个 std::string,可以使用 +:

与字符串文字连接
SetReport("Failure in " __FUNCTION__ ": foobar was " + foobar + "\n");

如果foobar是数字类型,可以用std::to_string(foobar)转换。

普通字符串文字(例如 "abc"__FUNCTION__)和 char const* 不支持连接。这些只是普通的 C 风格 char const[]char const*.

解决方案是使用一些字符串格式化工具或库,例如:

  • std::string 并使用 + 连接。可能涉及太多不必要的分配,除非 operator+ 使用表达式模板。
  • std::snprintf。这个不会为您分配缓冲区并且类型不安全,因此人们最终会为它创建包装器。
  • std::stringstream。无处不在且标准,但它的语法充其量是笨拙的。
  • boost::format。输入安全但据说速度很慢。
  • cppformat。据报道现代且快速。

最简单的解决方案之一是使用 C++ 空字符串。在这里,我声明了名为 _ 的空字符串变量,并在字符串连接之前使用了它。确保你总是把它放在前面。

#include <cstdio>
#include <string>

using namespace std;
string _ = "";

int main() {
        char s[] = "chararray";
        string result =
                _ + "function name = [" + __FUNCTION__ + "] "
                "and s is [" + s + "]\n";
        printf( "%s", result.c_str() );
        return 0;
}

输出:

function name = [main] and s is [chararray]

关于__FUNCTION__,我发现在Visual C++中它是一个宏,而在GCC中它是一个变量,所以SetReport("Failure in " __FUNCTION__ "; foobar was " + foobar + "\n");只适用于Visual C++。参见:https://msdn.microsoft.com/en-us/library/b0084kay.aspx and https://gcc.gnu.org/onlinedocs/gcc/Function-Names.html

上面使用空字符串变量的解决方案应该适用于 Visual C++ 和 GCC。

我的解决方案

我继续尝试不同的东西,我得到了一个解决方案,它结合了 tivn 的答案,其中涉及制作一个空字符串以帮助将长 std::string 和字符数组连接在一起,以及我自己的一个函数这允许将 std::string 单行复制到 const char*,当字符串对象离开范围时可以安全使用。

我会使用 Mike Seymour 的可变参数模板,但 Visual Studio 2012 似乎不支持它们,我是 运行,我需要这个解决方案非常通用,所以我可以'依靠他们。

这是我的解决方案:

Strings.h

#ifndef _STRINGS_H_
#define _STRINGS_H_

#include <string>

// tivn's empty string in the header file
extern const std::string _;

// My own version of .c_str() which produces a copy of the contents of the string input
const char* ToCString(std::string input);

#endif

Strings.cpp

#include "Strings.h"

const std::string str = "";

const char* ToCString(std::string input)
{
    char* result = new char[input.length()+1];
    strcpy_s(result, input.length()+1, input.c_str());
    return result;
}

用法

m_someMemberConstChar = ToCString(_ + "Hello, world! " + someDynamicValue);

我认为这非常简洁并且在大多数情况下都有效。谢谢大家帮我解决这个问题。

自 C++20 起,fmtlib 已进入 ISO 标准,但即使在较旧的迭代中,您仍然可以下载和使用它。

它提供与 Python 的 str.format()(a) 类似的功能,然后您的“丑陋的字符串”示例变得相对简单:

#include <fmt/format.h>

// Later on, where code is allowed (inside a function for example) ...

SetReport(fmt::format("Failure in {}: foobar was {}\n", __FUNCTION__, foobar));

它很像 printf() 系列,但内置了可扩展性和类型安全性。


(a) 但是,不幸的是,不是它的字符串插值功能(使用 f 字符串),它具有将表达式放在字符串中的附加优势它们的输出位置,例如:

set_report(f"Failure in {__FUNCTION__}: foobar was {foobar}\n");

如果 fmtlib 曾经拥有那种能力,我可能会兴奋地尿裤子:-)