使用std::string,std::wstring作为缓冲区的C++问题

4

在使用WinAPI时,你经常会遇到一些方法需要将LPWSTR或LPSTR作为参数传递。有时这个指针实际上应该是指向缓冲区的指针,例如:

  int GetWindowTextW(HWND hWnd, LPWSTR lpString, int nMaxCount);

对于这样的缓冲区,使用std::wstring是否是一个好主意呢?在特定情况下,我强烈需要产生std::wstring作为结果,不能用vector<wchar_t>等替代方法吗?

std::wstring myWrapper(HWND hWnd){
    auto desiredBufferSize = GetWindowTextLengthW(hWnd);
    std::wstring resultWstr;
    resultWstr.resize(desiredBufferSize);
    auto ret = GetWindowText(hWnd,
                             const_cast<wchar_t*>(resultWstr.data()), // const_cast
                             resultWstr.size());
    // handle return code code
    return resultWstr;
}

data()c_str() 字符串方法返回 const 指针,因此我们必须使用 const_cast 来移除 constness,但有时这是一个不好的信号。在这种情况下这样做是否明智?我能做得更好吗?


4
使用 &str[0],只有在 C++11 中才能保证正常运作。 - chris
如果代码被没有独占传递的hWnd的线程调用,两次调用之间长度可能会发生更改... 如果标准容器有一种方法可以在不设置新元素的情况下增长到指定大小,那就太好了... - Deduplicator
@Deduplicator 这是简化的代码,仅用于指定问题。这些特定的WinAPI方法仅作为示例。 - vard
1
我认为你在这里有一个偏移量错误:GetWindowTextLength返回长度(据我所知,不包括终止的空字符)。GetWindowText将其第三个参数作为缓冲区字符计数,包括终止的空字符。std::basic_string(在>= C++11中)不允许修改终止的空字符,因此你必须为此额外留出空间。(这意味着你最终会得到两个空字符,所以basic_string可能不是最好的工具。) - dyp
@chris:不,连续缓冲区的事情是在2004或2005年左右的Lillehammer会议上被通过的。当时,委员会所知道的所有现存实现都有连续的缓冲区,当然自那时以来也没有任何打破这个规定的实现被创建。因此,这也适用于C++03。 - Cheers and hth. - Alf
显示剩余5条评论
3个回答

3

将字符串用作C-字符串

const char*std::string有自动类型转换,但反过来则没有。

字符“\0”对于std::string并不特殊。

  • 使用&s[0]进行写访问

确保字符串大小(而不仅仅是容量)足够进行C风格的写入。

  • 使用s.c_str()进行只读访问

只在下一次调用非常量方法之前有效。

代码示例:

const int MAX_BUFFER_SIZE = 30;         // Including NULL terminator.         
string s(MAX_BUFFER_SIZE, '\0');      // Allocate enough space, NULL terminated
strcpy(&s[0], "This is source string.");    // Write, C++11 only (VS2010 OK)
printf("C str: '%s'\n", s.c_str());     // Read only: Use const whenever possible.

1
在示例代码中,您忘记调整字符串长度以匹配复制的C字符串。此外,对于非宏名称使用全大写是不好的。因为(1)它很难看,(2)与宏名称的常见约定冲突,可能会导致意外的文本替换。 - Cheers and hth. - Alf
strcpy复制到std::string并不是一个好主意。现在使用strcpy本身也不是一个好主意。 - Jonathan Potter
strcpy(&s[0], "This is source string."); // 这行代码仅是演示如何将传统API与std::string接口连接起来。 - Garland

2
很诱人去使用漂亮的标准wstring。然而,强制转换常量永远不是好方法...
这里有一个临时字符串包装器,它自动创建缓冲区,将其指针传递给winapi函数,并将缓冲区的内容复制到您的字符串中,然后干净地消失:
auto ret = GetWindowText(hWnd,
                         tmpstr (resultWstr, desiredBufferSize), 
                         resultWstr.size());

这个解决方案适用于任何在返回之前写入字符指针的Windows API函数(即没有异步操作)。 它是如何工作的? 它基于C++标准§12.2点3:“临时对象在评估包含它们创建点的完整表达式的最后一步中被销毁。 (...) 销毁临时对象的值计算和副作用仅与完整表达式相关,而不与任何特定子表达式相关。”。
以下是它的实现:
typedef std::basic_string<TCHAR> tstring;  // based on microsoft's TCHAR

class tmpstr {
private:
    tstring &t;      // for later cpy of the result
    TCHAR *buff;     // temp buffer 
public:
    tmpstr(tstring& v, int ml) : t(v) {     // ctor 
          buff = new TCHAR[ml]{};           // you could also initialize it if needed
           std::cout << "tmp created\n";    // just for tracing, for proof of concept
        }
    tmpstr(tmpstr&c) = delete;              // No copy allowed
    tmpstr& operator= (tmpstr&c) = delete;  // No assignment allowed
    ~tmpstr() {                              
          t = tstring(buff);                // copy to string passed by ref at construction
          delete buff;                      // clean everyhing
          std::cout<< "tmp destroyed";      // just for proof of concept.  remove this line
        }
    operator LPTSTR () {return buff; }  // auto conversion to serve as windows function parameter without having to care
}; 

如您所见,第一行使用了typedef,以便与多个Windows编译选项兼容(例如Unicode或非Unicode)。但是,如果您愿意,当然可以将和替换为和。

唯一的缺点是您必须在tmpstr构造函数的参数和Windows函数的参数中重复缓冲区大小。但这就是您编写函数包装器的原因,不是吗?

1
我会编写一个 GetWindowText 替代函数,它可以在内部处理所有内容并返回一个 std::string。使用 GetWindowTextLength 来计算需要多大的缓冲区。 - Jonathan Potter
1
@JonathanPotter 实际上,这就是OP想要实现的。我在这里提出的解决方案的优点是,它可以与任何通过LPTSTR(或LPSTR或LPWSTR)返回字符串的Windows API函数一起使用,唯一的条件是在返回之前写入字符串(即非异步)。 - Christophe
@Cristophe,这种方法可能的缺点是字符串的实际复制会执行两次 - 首先在winAPI调用中,然后在包装器析构函数中。不是吗? - vard
是的,这就是尊重字符串数据的常量性所带来的后果。 - Christophe
1
请删除/禁用复制构造函数和赋值运算符。它们无法按预期工作。 - dyp
@dyp 是的!好主意!它应该被用作临时变量,所以不应该发生复制或赋值,但你是对的:最好预防一下。我会进行编辑。 - Christophe

1
为什么不只使用字符数组来作为字符串缓冲区呢? :)
DWORD username_len = UNLEN + 1;
vector<TCHAR> username(username_len);
GetUserName(&username[0], &username_len);

被接受的解决方案是过度思考的好例子。


是的,但是相比Garland的解决方案,你的解决方案如何不过度思考呢?他们的解决方案是一样的,只是没有掩盖“字符串”的概念。(尽管他的措辞令人遗憾地不够明确和混乱。) - Sz.
由于 strcpy(&s[0] ... ,为什么要对数组的第一个项进行解引用?因为数组名会给出其第一个成员的地址。2)为什么不使用stdlib?专业代码应该干净整洁。 - Igor Zinin
GetUserName(username.data(), username.size()); // 可能需要将第二个参数的 size_t 强制转换为 int - vt.

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