mentalmushroom
mentalmushroom

Reputation: 2467

C++ static variable initialization inside a template function

I noticed quite a strange behavior of static variable initialization in function templates. Consider the following example:

MyFile * createFile()
{
    std::cout << "createFile" << std::endl;
    return nullptr;
}

template <typename T>
void test(const T& t)
//void test(T t)
{
    static MyFile *f = createFile();
}

void main()
{
    test("one");
    //test("two");
    test("three");
}

As long as f in test is static, I expected createFile to be called only once. However, it is called twice.

Having spent some time playing around with the problem, I noticed that removing const reference from the argument in test fixes it. Another interesting thing is that the length of the string passed to the function also affects the initialization: when the length of parameters is equal, static variable is initialized only once, otherwise, a new initialization takes place.

Could somebody explain this? Solutions/workarounds apart from the mentioned ones are very welcome.

Upvotes: 7

Views: 504

Answers (2)

Richard Hodges
Richard Hodges

Reputation: 69882

The literal "one" is a const char [4].

this code:

test("one")

would ideally like to call test(const char (&)[4])

This works for test(const T&) (because const char (&) [4] can bind to const char (const&) [4]).

But it cannot work for test(T t) because you can't pass string literals by value. They are passed by reference.

However, const char[4] can decay to const char*, which can match template<class T> void func(T t).

The proof is in the pudding:

#include <cstdint>
#include <iostream>
#include <typeinfo>

template <typename T, std::size_t N>
void test_const(const T(&t)[N])
{
    std::cout << __func__ << " for literal " << t << " T is a " << typeid(T).name() << " and N is " << N << std::endl;
}

template <typename T>
void test_mutable(T &t)
{
    std::cout << __func__ << " for literal " << t << " T is a " << typeid(T).name() << std::endl;
}

template <typename T>
void test_const_ref(const T &t)
{
    std::cout << __func__ << " for literal " << t << " T is a " << typeid(T).name() << std::endl;
}

template <typename T>
void test_copy(T t)
{
    std::cout << __func__ << " for literal " << t << " T is a " << typeid(T).name() << std::endl;
}

int main()
{
    test_const("one");
    test_const("three");
    test_mutable("one");
    test_mutable("three");
    test_const_ref("one");
    test_const_ref("three");
    test_copy("one");
    test_copy("three");
}

example results (clang):

test_const for literal one T is a c and N is 4
test_const for literal three T is a c and N is 6
test_mutable for literal one T is a A4_c
test_mutable for literal three T is a A6_c
test_const_ref for literal one T is a A4_c
test_const_ref for literal three T is a A6_c
test_copy for literal one T is a PKc
test_copy for literal three T is a PKc

Here is a version with demangled names (will compile on clang and gcc):

#include <cstdint>
#include <iostream>
#include <typeinfo>
#include <cstdlib>
#include <cxxabi.h>

std::string demangle(const char* name)
{
    int status = -1;
    // enable c++11 by passing the flag -std=c++11 to g++
    std::unique_ptr<char, void(*)(void*)> res {
        abi::__cxa_demangle(name, NULL, NULL, &status),
        std::free
    };

    return (status==0) ? res.get() : name ;
}

template <typename T, std::size_t N>
void test_const(const T(&t)[N])
{
    std::cout << __func__ << " for literal " << t << " T is a " << demangle(typeid(T).name()) << " and N is " << N << std::endl;
}

template <typename T>
void test_mutable(T &t)
{
    std::cout << __func__ << " for literal " << t << " T is a " << demangle(typeid(T).name()) << std::endl;
}

template <typename T>
void test_const_ref(const T &t)
{
    std::cout << __func__ << " for literal " << t << " T is a " << demangle(typeid(T).name()) << std::endl;
}

template <typename T>
void test_copy(T t)
{
    std::cout << __func__ << " for literal " << t << " T is a " << demangle(typeid(T).name()) << std::endl;
}

int main()
{
    test_const("one");
    test_const("three");
    test_mutable("one");
    test_mutable("three");
    test_const_ref("one");
    test_const_ref("three");
    test_copy("one");
    test_copy("three");
}

expected output:

test_const for literal one T is a char and N is 4
test_const for literal three T is a char and N is 6
test_mutable for literal one T is a char [4]
test_mutable for literal three T is a char [6]
test_const_ref for literal one T is a char [4]
test_const_ref for literal three T is a char [6]
test_copy for literal one T is a char const*
test_copy for literal three T is a char const*

Upvotes: 6

Serge Ballesta
Serge Ballesta

Reputation: 148900

As a complement to @RichardHodges's answer that explains why different instanciations are used, it is easy to force only one, because arrays can decay to pointer with an explicit template instanciation:

test<const char *>("one");
test<const char *>("two");
test<const char *>("three");

result in one single call of createFile.

In fact (as said in comment by BoBTFish), that is exactly what happens when you write:

template <typename T>
void test(const T t)

Whatever the size of the array, the array automatically decays to a const char * because C++ does not allow to assign directly arrays.

BTW, void main() is bad. Always use int main() and an explicit return.

Upvotes: 0

Related Questions