7

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.

4
  • 6
    You are instantiating the template with 2 different Ts, a char[4] ("one") and a char[6] ("three"). Therefore you have 2 new types, both containing an f, therefore both of those fs must be initialised. Hence createFile() is called twice. Commented Jan 11, 2017 at 16:13
  • 1
    Ok, but why simple template version (without const ref) works? Commented Jan 11, 2017 at 16:15
  • 1
    Pointer decay (you can't take an array by value). Which is not simple enough to describe in a comment. I'll let someone else describe it in an answer. Commented Jan 11, 2017 at 16:16
  • @mentalmushroom: Becuse it creates the same const char * instantiation for all literals. The reference is the critical piece in this case that makes the length of the literal part of function type. Commented Jan 11, 2017 at 17:08

2 Answers 2

6

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*
Sign up to request clarification or add additional context in comments.

8 Comments

@Quentin it cant' deduce that. That would make the argument const const char &
Woops, let me try again. It deduces const char[4] for T :)
@Quentin the leading const prevents it. It would be const const char[4] which is not legal
Apparently... It's char[4]. I'm stumped. (edit: the order of const T does not change anything)
But, but, but. You argue test(const T&) will match const char * - and thus the static will be constructed once ... but test(T&) will match const char[N], and thus the static will be constructed twice. The issue is that the OP reports the opposite.
|
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.