Thread: Passing C-style string to template function as reference

  1. #1
    Registered User
    Join Date
    Jan 2017
    Posts
    6

    Passing C-style string to template function as reference

    I was reading about std::decay on Stack Overflow (c++ - What is std::decay and when it should be used? - Stack Overflow) and in the accepted answer it is stated that:
    If it accepted its parameters by reference, then T1 will be deduced as an array type, and then constructing a pair<T1, T2> will be ill-formed.
    The following code demonstrates the issue I have:
    Code:
    #include <iostream>
    #include <typeinfo>
    
    template <class T1, class T2>
    void f(T1 &x, T2 y)
    {
        std::cout << typeid(x).name() << '\n';
        std::cout << typeid(y).name() << '\n';
    }
    
    int main()
    {
        f("foo", "bar");
        return 0;
    }
    Running this with VS 2015 gives me the following:
    Code:
    char const [4]
    char const *
    I don't really understand why T1 is being deduced as an array type. "foo" has type const char* so does this mean x has type const char*& which evaluates to const char array? I can see that the type T1 contains size information so it's definitely an array type and no array decay happened but what I'm having trouble with is the syntax and how adding the reference operator results in an array and removes the implicit cast.

    Cheers!

  2. #2
    Registered User
    Join Date
    Apr 2006
    Posts
    2,149
    Literal "foo" actually has type "char const [4]". Therefore T1 is the same, and x is of type "char const (&)[4]", though typeid doesn't say this because it can't make the distinction.

    With "bar", the type of T2 would be 'char const [4]". However, declaring a function argument like this actually declares it as a char pointer. So the type of T2 and y is "char const *".

    It may help to understand if you replace the template parameters with their actual type:
    Code:
    #include <iostream>
    #include <typeinfo>
     
    void f(const char (&x)[4], const char y[4])
    {
        std::cout << typeid(x).name() << '\n';
        std::cout << typeid(y).name() << '\n';
    }
     
    int main()
    {
        f("foo", "bar");
        return 0;
    }
    Last edited by King Mir; 01-11-2017 at 08:02 AM.
    It is too clear and so it is hard to see.
    A dunce once searched for fire with a lighted lantern.
    Had he known what fire was,
    He could have cooked his rice much sooner.

  3. #3
    Registered User
    Join Date
    Jan 2017
    Posts
    6
    That helped me but also confused me. In your example if "bar" has type char const [4] and I'm passing it by value to f, and if y also has type char const [4] then why is typeid saying it's char const *? sizeof(y) is 4 (size of pointer, not 4 x 1 byte chars) so we're losing size information. I thought this only happens when an array is implicitly casted to a pointer which happens when trying and use an array in a pointer context (which isn't the case here). I hope I've been able to communicate why I'm confused... Basically I'm expecting both x and y to have type const char [4], both with size 4 (4 x 1 byte chars) and the only difference being one is passed by reference and the other is copied.

  4. #4
    Registered User
    Join Date
    Jan 2017
    Posts
    6
    I've managed to figure out what's going on although I'd still appreciate it if someone has anything to add

    Code:
    void a(const int *test);
    void b(const int test[]);
    void c(const int test[10]);
    Turns out these three function declarations are identical and in each case, passing an array will result in the array decaying into a pointer. The compiler ignores the 10 I've added in declaration of c and this was the source of confusion for me.

    The only way to pass an actual array (without decay) is by using reference:
    Code:
    void d(const int(&test)[10])
    The compiler will complain if I don't specify the size in declaration of d, or if I pass an array that has size other than 10.

    Alternatively I can pass a pointer to an array:
    Code:
    void alternative(const int (*test)[10]){
        std::cout << typeid(*test).name() << '\n';
        std::cout << sizeof(*test) << "\n";
    }
    Here I can only pass an int array of size 10. typeid().name() gives int const [10] and sizeof() gives 40 on my machine.

    Also learnt that even though the compiler ignores the specified bound in the case of c, it actually only ignores the first (left-most) bound.

    Code:
    void e(const int test[32][32][32]){ // identical to void e(const int test[][32][32])
        std::cout << typeid(test).name() << '\n';
        std::cout << sizeof(test) << "\n";
    }
    
    int main()
    {
        int test[64][32][32]; // 64 can be replaced by any valid value (>0) but the other two sizes must be 32
        e(test);
        return 0;
    }
    This prints int const (*)[32][32] and 4.
    Last edited by sythical; 01-11-2017 at 01:07 PM.

  5. #5
    Informer -Adrian's Avatar
    Join Date
    Jan 2013
    Posts
    811
    if y also has type char const [4] then why is typeid saying it's char const *
    The two statements are semantically identical, so
    Code:
    void f(int* n);
    void f(int n[5]); // results in  int*  like the preceding line, C/C++ is weird like that
    Try and compile the above and you should see a function redefinition error.

    Passing an array by reference–as you saw–preserves the whole type with its size.
    Last edited by -Adrian; 01-11-2017 at 01:05 PM. Reason: You just posted (what are the odds), well i'll just keep it here.

  6. #6
    C++まいる!Cをこわせ!
    Join Date
    Oct 2007
    Location
    Inside my computer
    Posts
    24,654
    Quote Originally Posted by sythical View Post
    I've managed to figure out what's going on although I'd still appreciate it if someone has anything to add
    This whole weird and stupid behaviour is due to C++'s C legacy. Basically, the C standard says it shall work as you see it works. C++ fixed the problem with reference types, so passing an array by reference works as expected with no loss of type information.

    To be honest with you, this is (one of) the reasons why we shouldn't use C arrays: their semantics are confusing, dumb and stupid. Objects always follow the same rules that applies to every type except for C-style arrays that have exceptions. For strings, use string_view in the functions instead.
    Quote Originally Posted by Adak View Post
    io.h certainly IS included in some modern compilers. It is no longer part of the standard for C, but it is nevertheless, included in the very latest Pelles C versions.
    Quote Originally Posted by Salem View Post
    You mean it's included as a crutch to help ancient programmers limp along without them having to relearn too much.

    Outside of your DOS world, your header file is meaningless.

  7. #7
    Its hard... But im here swgh's Avatar
    Join Date
    Apr 2005
    Location
    England
    Posts
    1,687
    True - colleges and some 'older' books do still enforce people learning C++ to learn C style arrays before Vectors. However vectors are part of the STL and perhaps authors of those books or lecturers would rather teach vectors as part of the whole "STL package". Still, C arrays do have their place in C++ even if it's just a simple learning tool into how simple data structures work.

    I've read a few lecture notes from as recently as 2016 and most still explain C arrays before vectors are introduced. However, Java has the same problem in this regard - most texts show arrays as objects (as they do in Java) before moving onto Array Lists - which is just a container array type in Java. C# has lists. It's probably all connected to the "learn A before B" theory.
    Double Helix STL

  8. #8
    Registered User MutantJohn's Avatar
    Join Date
    Feb 2013
    Posts
    2,665
    What's even more silly is that we're still using raw arrays when we should be using std::array. It's a very thin layer of abstraction of the native array that makes it useful and also ensures your code will still work even when ranges become a more popularized and dominate abstraction.

  9. #9
    Registered User
    Join Date
    Apr 2006
    Posts
    2,149
    Saying you should use std::array is well and good, but doesn't help the problem here, because we're dealing with string literals. So understanding the intricacies here can be necessary. Often though, you can just provide an overload or template specialization specifically for strings, and avoid the gotcha that way. A non-template overload will always be preferred to a template function, so that can be a way to make sure all strings are treated the same.

    Quote Originally Posted by sythical View Post
    I've managed to figure out what's going on although I'd still appreciate it if someone has anything to add
    Bonus question: What if you do this?
    Code:
    #include <iostream>
    #include <typeinfo>
     
    template <class T1, class T2>
    void f(T1 &x, T2 y)
    {
        T2 z = "baz";
        std::cout << typeid(x).name() << '\n';
        std::cout << typeid(y).name() << '\n';
        std::cout << typeid(z).name() << '\n';
    }
     
    int main()
    {
        f("foo", "bar");
        return 0;
    }
    Do you think the type of z should be "char const *" or "char const[4]"? Try it and see.

    Why it it that way? Because the writers of the standard though this would be most intuitive and useful. As all this demonstrates, array types are treated specially by template type deduction.
    Last edited by King Mir; 01-11-2017 at 05:20 PM.
    It is too clear and so it is hard to see.
    A dunce once searched for fire with a lighted lantern.
    Had he known what fire was,
    He could have cooked his rice much sooner.

  10. #10
    Registered User MutantJohn's Avatar
    Join Date
    Feb 2013
    Posts
    2,665
    Ah, that's right. We're passing raw strings around. So yes, std::array isn't exactly applicable here, my apologies.

    But, imo, raw arrays are problematic exactly for the reason why this thread was created. It's because it's difficult to reason about the behavior of the type as it's converted or referenced. Using something like std::string is significantly more reliable in this instance and supports short string optimization which will favor the stack when it's convenient so you don't lose much (if anything) with regards to performance and gain a significantly more clear view of what's happening.

  11. #11
    Registered User
    Join Date
    Apr 2006
    Posts
    2,149
    Unfortunately, unlike with std::array, you can't just replace all uses of "char *" with std::string, as the OP example demonstrates.

    Also, because string is a dynamically sized, it has costs that std::array doesn't. Elysia mentioned C++17's std::string_view, which is cheaper, but still not as cheep as an array reference. And a const char * may be relatively simple and cheep like array references when you don't need to know the size. So there are choices and trade offs.
    It is too clear and so it is hard to see.
    A dunce once searched for fire with a lighted lantern.
    Had he known what fire was,
    He could have cooked his rice much sooner.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Replies: 6
    Last Post: 09-13-2013, 04:13 PM
  2. Missing reference in function template?
    By Inanna in forum C++ Programming
    Replies: 20
    Last Post: 05-30-2011, 02:26 AM
  3. undefined reference to template function
    By Elkvis in forum C++ Programming
    Replies: 5
    Last Post: 09-02-2009, 08:13 AM
  4. Passing a set to a template function
    By cloudy in forum C++ Programming
    Replies: 1
    Last Post: 10-09-2007, 06:57 AM
  5. template with function-style parameter
    By R.Stiltskin in forum C++ Programming
    Replies: 2
    Last Post: 05-20-2003, 10:56 PM

Tags for this Thread