Кэширование const char * в качестве типа возврата - PullRequest
5 голосов
/ 14 октября 2008

Читал немного на моем C ++ и нашел эту статью о RTTI (Идентификация типов времени выполнения): http://msdn.microsoft.com/en-us/library/70ky2y6k(VS.80).aspx. Ну, это еще одна тема :) - Однако я наткнулся на странное высказывание в type_info -классе, а именно о ::name -методе. В нем говорится: «Функция-член type_info::name возвращает const char* для строки с нулевым символом в конце, представляющей понятное человеку имя типа. Указанная память кэшируется и никогда не должна быть непосредственно освобождена».

Как вы можете реализовать что-то подобное самостоятельно !? Раньше я довольно часто боролся с этой проблемой, так как не хочу создавать новый char массив для вызывающей стороны, поэтому до сих пор придерживаюсь std::string.

Итак, ради простоты, скажем, я хочу создать метод, который возвращает "Hello World!", назовем его

const char *getHelloString() const;

Лично я бы сделал это как-то так (Псевдо):

const char *getHelloString() const
{
  char *returnVal = new char[13];
  strcpy("HelloWorld!", returnVal);

  return returnVal
}

.. Но это будет означать, что вызывающая сторона должна сделать delete[] для моего указателя возврата: (

Спасибо заранее

Ответы [ 12 ]

23 голосов
/ 14 октября 2008

Как насчет этого:

const char *getHelloString() const
{
    return "HelloWorld!";
}

Возврат литерала напрямую означает, что пространство для строки выделено компилятором в статическом хранилище и будет доступно на протяжении всей программы.

3 голосов
/ 15 октября 2008

Мне нравятся все ответы о том, как строка может быть статически размещена, но это не обязательно верно для всех реализаций, особенно для той, чья документация связана с оригинальным постером. В этом случае создается впечатление, что декорированное имя типа хранится статически для экономии места, а недекорированное имя типа вычисляется по требованию и кэшируется в связанном списке.

Если вам интересно, как реализация Visual C ++ type_info::name() распределяет и кэширует свою память, выяснить несложно. Сначала создайте крошечную тестовую программу:

#include <cstdio>
#include <typeinfo>
#include <vector>    
int main(int argc, char* argv[]) {
    std::vector<int> v;
    const type_info& ti = typeid(v);
    const char* n = ti.name();
    printf("%s\n", n);
    return 0;
}

Соберите его и запустите под отладчиком (я использовал WinDbg) и посмотрите на указатель, возвращаемый type_info::name(). Это указывает на глобальную структуру? Если это так, команда WinDbg ln сообщит имя ближайшего символа:

0:000> ?? n
char * 0x00000000`00857290
 "class std::vector<int,class std::allocator<int> >"
0:000> ln 0x00000000`00857290
0:000>

ln ничего не печатал, что указывает на то, что строка не была в диапазоне адресов, принадлежащих какому-либо конкретному модулю. Он был бы в этом диапазоне, если бы он был в сегменте данных или данных только для чтения. Давайте посмотрим, был ли он выделен в куче, выполнив поиск во всех кучах по адресу, возвращенному type_info::name():

0:000> !heap -x 0x00000000`00857290
Entry             User              Heap              Segment               Size  PrevSize  Unused    Flags
-------------------------------------------------------------------------------------------------------------
0000000000857280  0000000000857290  0000000000850000  0000000000850000        70        40        3e  busy extra fill 

Да, это было выделено в куче. Установка точки останова в начале malloc() и перезапуск программы подтверждают это.

Просмотр объявления в <typeinfo> дает представление о том, где кэшируются указатели кучи:

struct __type_info_node {
    void *memPtr;
    __type_info_node* next;
};

extern __type_info_node __type_info_root_node;
...
_CRTIMP_PURE const char* __CLR_OR_THIS_CALL name(__type_info_node* __ptype_info_node = &__type_info_root_node) const;

Если вы найдете адрес __type_info_root_node и пройдете по списку в отладчике, вы быстро найдете узел, содержащий тот же адрес, который был возвращен type_info::name(). Список, похоже, связан со схемой кэширования.

Страница MSDN, указанная в исходном вопросе, по-видимому, заполняет пробелы: имя сохраняется в его оформленной форме для экономии места, и эта форма доступна через type_info::raw_name(). Когда вы впервые вызываете type_info::name() для данного типа, он отменяет декорирование имени, сохраняет его в буфере, выделенном в куче, кэширует указатель буфера и возвращает его.

Связанный список может также использоваться для освобождения кэшированных строк при выходе из программы (однако я не проверял, так ли это). Это гарантирует, что они не будут отображаться как утечки памяти при запуске инструмента отладки памяти.

2 голосов
/ 14 октября 2008

Ну и дела, если мы говорим только о функции, вы всегда хотите вернуть одно и то же значение. это довольно просто.

const char * foo() 
{
   static char[] return_val= "HelloWorld!";
   return return_val;
}

Хитрость заключается в том, что когда вы начинаете делать вещи, где вы кэшируете результат, а затем вы должны учитывать многопоточность, или когда ваш кеш становится недействительным, и пытаться сохранить вещи в локальном хранилище потоков. Но если это просто однократный вывод, который копируется немедленно, это должно сработать.
С другой стороны, если у вас нет фиксированного размера, вы должны сделать что-то, где вам придется либо использовать статический буфер произвольного размера ... в котором вы можете в конечном итоге получить что-то слишком большое, либо обратиться к управляемому классу, скажем std::string.

const char * foo() 
{
   static std::string output;
   DoCalculation(output);
   return output.c_str();
}

также подпись функции

const char *getHelloString() const;

применимо только для функций-членов. В этот момент вам не нужно иметь дело с локальными переменными статической функции, и вы можете просто использовать переменную-член.

1 голос
/ 14 октября 2008

Будьте осторожны при реализации функции, которая выделяет кусок памяти, а затем ожидает, что вызывающая сторона освободит его, как вы делаете в OP:

const char *getHelloString() const
{
  char *returnVal = new char[13];
  strcpy("HelloWorld!", returnVal);

  return returnVal
}

При этом вы передаете право собственности на память вызывающей стороне. Если вы вызываете этот код из какой-то другой функции:

int main()
{
  char * str = getHelloString();
  delete str;
  return 0;
}

... семантика передачи владения памятью неясна, создавая ситуацию, когда ошибки и утечки памяти более вероятны.

Кроме того, по крайней мере, под Windows, если две функции находятся в 2 разных модулях, вы можете потенциально испортить кучу. В частности, если main () находится в hello.exe, скомпилирован в VC9, а getHelloString () находится в utility.dll, скомпилирован в VC6, вы будете повреждать кучу при удалении памяти. Это связано с тем, что VC6 и VC9 используют свою собственную кучу, и они не являются одной и той же кучей, поэтому вы выделяете из одной кучи и освобождаете от другой.

1 голос
/ 14 октября 2008

Я думаю, что, поскольку они знают, что их существует конечное число, они просто держат их всегда. В некоторых случаях может быть целесообразно сделать это, но, как правило, std :: string будет лучше.

Они также могут искать новые вызовы, чтобы увидеть, сделали ли они эту строку уже и вернуть тот же указатель. Опять же, в зависимости от того, что вы делаете, это может быть полезно и вам.

0 голосов
/ 17 января 2010

Я думаю, что нечто подобное может быть реализовано только "чисто" с использованием объектов и идиомы RAII. Когда вызывается деструктор объектов (объект выходит из области видимости), мы можем смело предположить, что указатели const char* больше не используются.

пример кода:

class ICanReturnConstChars
{
    std::stack<char*> cached_strings
    public:
    const char* yeahGiveItToMe(){
        char* newmem = new char[something];
        //write something to newmem
        cached_strings.push_back(newmem);
        return newmem;
    }
    ~ICanReturnConstChars(){
        while(!cached_strings.empty()){
            delete [] cached_strings.back()
            cached_strings.pop_back()
        }
    }
};

Единственная известная мне возможность - передать smart_ptr ..

0 голосов
/ 14 октября 2008

Совет, который предупреждает о времени жизни возвращаемой строки, является разумным советом. Вы всегда должны быть осторожны в признании своих обязанностей, когда дело доходит до управления временем жизни возвращенных указателей. Однако эта практика вполне безопасна, если указанная переменная переживет вызов функции, которая ее вернула. Рассмотрим, например, указатель на const char, возвращаемый c_str() как метод класса std::string. Это возвращает указатель на память, управляемую строковым объектом, которая гарантированно будет действительной до тех пор, пока строковый объект не будет удален или сделан для перераспределения его внутренней памяти.

В случае класса std::type_info он является частью стандарта C ++, как следует из его пространства имен. Память, возвращаемая из name(), фактически указывает на статическую память, созданную компилятором и компоновщиком, когда класс был скомпилирован, и является частью системы идентификации типов во время выполнения (RTTI). Поскольку это относится к символу в кодовом пространстве, вы не должны пытаться удалить его.

0 голосов
/ 14 октября 2008

Примерно так:

const char *myfunction() {
    static char *str = NULL; /* this only happens once */
    delete [] str; /* delete previous cached version */
    str = new char[strlen("whatever") + 1]; /* allocate space for the string and it's NUL terminator */
    strcpy(str, "whatever");
    return str;
}

РЕДАКТИРОВАТЬ: Что-то, что пришло мне в голову, это то, что хорошей заменой для этого может быть возврат вместо boost :: shared_pointer. Таким образом, вызывающая сторона может удерживать ее столько, сколько пожелает, и им не нужно беспокоиться об ее явном удалении. Справедливый компромисс ИМО.

0 голосов
/ 14 октября 2008

Когда я нуждаюсь в такого рода функциях, я часто делаю, чтобы в классе был указатель char *, инициализированный нулем, и выделяемый при необходимости.

а именно:

class CacheNameString
{
    private: 
        char *name;
    public:
        CacheNameString():name(NULL)  { }

    const char *make_name(const char *v)
    {
        if (name != NULL)
            free(name);

        name = strdup(v);

        return name;
    }

};
0 голосов
/ 14 октября 2008

Почему тип возвращаемого значения должен быть const? Не думайте о методе как о get , думайте о нем как о create методе. Я видел множество API, которые требуют, чтобы вы удалили что-то, что возвращает оператор / метод создания. Просто убедитесь, что вы отметили это в документации.

/* create a hello string
 * must be deleted after use
 */
char *createHelloString() const
{
  char *returnVal = new char[13];
  strcpy("HelloWorld!", returnVal);

  return returnVal
}
...