65.9K
CodeProject 正在变化。 阅读更多。
Home

如何从 C++ 函数/方法返回字符串类

starIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIconemptyStarIcon

1.94/5 (5投票s)

2020年10月25日

CPOL

4分钟阅读

viewsIcon

17748

使用 STL 和 C++14 从函数/方法返回动态创建的字符串,该字符串会被自动垃圾回收。

引言

我遇到了一个问题,希望从 C++ API 方法返回一个动态生成的字符串,而无需强制 API 用户自己释放动态生成的字符串。

由于多年 C# 编程后,我的 C++ 知识已经非常生疏,我为此苦苦挣扎,并编写了一个中介类和一个适合此目的的最小化垃圾收集器,直到我意识到 STL 已经具备了所有这些功能。所有熟悉 STL 并且认为 std::unique_ptr<T> 是最佳伴侣的人,实际上可以到此为止了。如果仍然期望更多,也许可以继续阅读 解决 shared_ptr 问题的内存管理新方法 这篇文章,作者是 Achilleas Margaritis,或者 智能指针的构思(第 2 部分)。size == sizeof(std::shared_ptr)/2,作者是 weibing

接下来要讲的内容,更多是为那些 C++ 知识尚不发达,或者像我一样,已经非常生疏的人准备的。

背景


更新:此处描述的方法并不十分优雅,有些人可能会认为它不干净——而且总的来说,它确实如此。比返回 std::unique_ptr<T> 还有一个更好的方法——正如 Mircea NeacsuMichaelgor 在下方的评论中指出的那样。那么——为什么这个技巧仍然在这里?

优雅且干净的 C++ 解决方案的局限性

现代 C++ 提供了直接返回类实例的功能。我称之为“现代”C++,因为从函数/方法返回类实例需要实现移动赋值运算符。

假设我们有以下简单的情况

01 String provider()
02 {
03     LPWSTR pszRawData;
04     int iLengt = ::AnyOldFashionedCCall();
05     pszRawData = new WCHAR[pszRawData];
06     String s(pszRawData);
07     delete pszRawData;
08     return s;
09 }
10
11 void consumer()
12 {
13     /* do something */
14     String value = provider();
15     /* do something */
16 }

如果 class String 提供了第 14 行使用的移动赋值运算符,那么这将正常工作。

String value = provider();

一个可以让我们返回类实例的移动赋值运算符可能看起来像

/// <summary>
/// Assigns a (fresh) value to this instance of this <c>String</c> instance.
/// Acts like a move constructor (acquires the data / mediator's data will be incorporated).
/// </summary>
/// <param name="rstrMediator">The source of the character sequence to be assigned.</param>
/// <returns>Returns a self reference.</returns>
/// <remarks>Acquires the data of the mediator.
/// Mediator will be incorporated and empty after this operation.</remarks>
String& operator=(String&& rstrMediator);
{
    if (this != &rstrMediator)
    {
        if (_wszText != NULL)
            ::CoTaskMemFree((LPVOID)_wszText);
        _wszText = rstrMediator._wszText;
        rstrMediator._wszText = (LPCOWSTR)NULL;
    }

    return *this;
}

到目前为止,一切都很好。这种解决方案的缺点是,对于具有显著更多成员变量的类,实现移动赋值运算符的开销可能非常大。或者,类/类的基类需要移动赋值运算符,但该类在无法或不应更改的库中。

因此,如果不想实现移动赋值运算符,或者无法实现,请将以下方法视为一个建议。


想法

我将此技巧标记为 C++14。但我在这里展示的内容也可以用旧的 auto_ptr<T> 来实现,它在 C++11 中被弃用,在 C++17 中被删除。它也适用于 C++11,只是您需要不使用 std::make_unique<T>()

std::unique_ptr<T> 文档包含

std::unique_ptr 是一个智能指针,它通过指针拥有并管理另一个对象,并在 unique_ptr 离开作用域时处理该对象。

该对象在以下任一情况发生时,使用关联的删除器进行处理:

  • 所管理的 unique_ptr 对象被销毁
  • 所管理的 unique_ptr 对象通过 operator=reset() 被赋值给另一个指针。

并且

std::unique_ptr 通常用于管理对象的生命周期,包括:

  • 通过保证在正常退出和异常退出时都进行删除,为处理具有动态生命周期的对象的类和函数提供异常安全性
  • 将独占拥有的动态生命周期对象的拥有权传递给函数
  • 从函数获取独占拥有的动态生命周期对象的拥有权
  • 作为可移动容器(如 std::vector)的元素类型,这些容器保存指向动态分配对象的指针(例如,如果需要多态行为)

换句话说:我曾经梦想的一切都已经可以在 STL 中找到:垃圾回收以及从函数/方法返回动态分配内存的可能性。

Using the Code

示例

这是一个使用示例

static std::unique_ptr<String> CPath::GetDirectoryName(String& strPath)
{
    LPCOWSTR_TRANSFERRED wszBuffer = CPath_GetCoDirectoryName(strPath.Value());
    std::unique_ptr<String> pMediator = std::make_unique<String>(wszBuffer);
    return pMediator;
}

在此示例中,C++ 包装类 CPathGetDirectoryName() 方法调用普通的 C API 函数 CPath_GetCoDirectoryName(),并将结果作为 std::unique_ptr<String> 返回。
对于 C++11 实现,必须省略 std::make_unique<String>(wszBuffer),相应的行将如下所示:

    std::unique_ptr<String> pMediator(new String(wszBuffer)); 

请阅读 改进 shared_ptr 这篇由 AlexZakharenko 撰写的技巧,了解为什么应该使用 std::make_unique<String>(wszBuffer)

示例 CPath 包装类 GetDirectoryName() 方法的消费者代码将如下所示:

   std::unique_ptr<String> strText = CPath::GetDirectoryName(strPath);
    SetToolTip(_pToolBar, uiCommandID, strText.get()->Value());

String 类的完整代码可以在文章 在 ReactOS 上运行的基本图标编辑器(因此也适用于 Windows XP 及更高版本) 中找到。

构造函数

以下是一些示例构造函数实现

String::String(std::unique_ptr<String> mediator)
{
    if (mediator != NULL)
        _wszText = (LPCOWSTR)CString_CoCopy(mediator.get()->Value());
    else
        _wszText = (LPCOWSTR)NULL;
}

String::String(LPCOCWSTR /* weak */ wszText)
{
    _wszText = (LPCOWSTR)CString_CoCopy(wszText);
}

String::String(LPCOWSTR_TRANSFERRED wszText)
{
    _wszText = (LPCOWSTR)wszText;
}

String 类提供三个初始化构造函数

  1. 在字符串是函数/方法的返回值的情况下。
  2. 在不应接管字符数组所有权的情况下。
  3. 在应接管字符数组所有权的情况下。

赋值运算符

以及更多示例方法实现

String& String::operator=(std::unique_ptr<String> mediator)
{
    if (_wszText != NULL)
        ::CoTaskMemFree((LPVOID)_wszText);
    if (mediator != NULL)
        _wszText = (LPCOWSTR)CString_CoCopy(mediator.get()->Value());
    else
        _wszText = (LPCOWSTR)NULL;

    return *this;
}

String& String::operator=(LPCOCWSTR /* weak */ wszText)
{
    _wszText = (LPCOWSTR)CString_CoCopy(wszText);

    return *this;
}

String& String::operator=(const String& strText)
{
    _wszText = (LPCOWSTR)CString_CoCopy(strText.Value());

    return *this;
}

String 类还提供三个赋值运算符

  1. 在字符串是函数/方法的返回值的情况下。
  2. 在不应接管字符数组所有权的情况下。
  3. 在应接管字符数组所有权的情况下。

误区

但请注意,在字符串不是作为参数传递到函数/方法中,而是在函数/方法内部创建的情况下:

std::unique_ptr<String> String::Format(std::unique_ptr<String> strFormatMediator,
                                       CRttiProvider& s00, CRttiProvider& s01,
                                       CRttiProvider& s02, CRttiProvider& s03)
{
    if (strFormatMediator.get()->IsNullOrEmpty())
        return NULL;

    // Keep the data alive during this method's whole lifetime.
    std::unique_ptr<String> pS00 = s00.ToString();
    std::unique_ptr<String> pS01 = s01.ToString();
    std::unique_ptr<String> pS02 = s02.ToString();
    std::unique_ptr<String> pS03 = s03.ToString();

    LPCWSTR pXX[4];
    pXX[0] = pS00.get()->Value();
    pXX[1] = pS01.get()->Value();
    pXX[2] = pS02.get()->Value();
    pXX[3] = pS03.get()->Value();

    return Format(strFormatMediator.get()->Value(), (LPCWSTR*)pXX, (WORD)4);
}

在这种情况下,必须确保 std::unique_ptr<String> 有足够的生命周期。

关注点

我的代码如何才能“干净”,即“没有内存泄漏”,同时又方便使用?

历史

  • 2020年10月25日:初版
  • 2020年10月26日:更新 1
  • 2020年12月28日:更新 2
© . All rights reserved.