Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Is using an address of a declared but not defined function as template argument valid?

34 views
Skip to first unread message

kmo

unread,
Apr 23, 2018, 8:11:37 AM4/23/18
to
Please consider the following code:

CODE START----------------------------------------

#include<stdio.h>

int f1(int, int);
int f2(int, int);
int f3(int);

template<typename T, T *ptr>
inline void *fid() {
return (void *)&fid<T, ptr>;
};

int main() {
printf("%p\n", fid<decltype(f1), f1>());
printf("%p\n", fid<decltype(f2), f2>());
printf("%p\n", fid<decltype(f3), f3>());
return 0;
};

CODE END------------------------------------------


fid takes a function pointer as template parameter and returns the pointer to
itself, making a kind of numeric identifer of a declaration.
This is the entire source code, no additonal files are present.
f1, f2, f3 are only delcared here, no definition is given.

The question is:
a) is this code valid (should this code compile)?
b) if it is, what should it output: are three different pointers guaranteed?
c) if it is not, why?
Or, maybe, it is a some case of undefined behavior / no diagnostics required?

My reasoning goes this way:
Main question: Are f1, f2, f3 ODR used?
If yes, the code sould not compile.
If not, fid instances are different objects, so they should have different
addresses.

Visual Studio 2017 (15.6.6) compiles and prints 3 equal pointers,
unless you compile it with /GL (whole program optimization) in which case
linker complains about unresolved f1, f2, f3.
However, replacing every fid<decltype(f*), f*> with fid<decltype(f*), &f*>
for *=1,2,3 silences the linker and the output is 3 equal pointers.

Clang (bundled with the mentioned Visual Studio, LLVM-2014) also compiles, prints
3 different pointers. Compiling in Release produces 3 equal pointers.

g++ 4.8.1 (MinGW) compiles and prints 3 different pointers.
g++ 5.2.1 (Ubuntu 15.10) compiles, 3 different pointers.
clang++ 3.6.2 (Ubuntu 15.10) compiles, 3 different pointers.

Compilers on Compiler Explorer compile it just fine, but probably the code
is only compiled, not linked (G++ 7.3, ICC 18.0.0, CLang 6.0.0, MSVC 19 2017 RTW).

So the majority says a) yes, b) different, but is the majority correct?

James R. Kuyper

unread,
Apr 23, 2018, 9:31:04 AM4/23/18
to
On 04/23/2018 08:11 AM, kmo wrote:
> Please consider the following code:
>
> CODE START----------------------------------------
>
> #include<stdio.h>
>
> int f1(int, int);
> int f2(int, int);
> int f3(int);
>
> template<typename T, T *ptr>
> inline void *fid() {
> return (void *)&fid<T, ptr>;
> };
>
> int main() {
> printf("%p\n", fid<decltype(f1), f1>());

Note 1: "%p" requires a void* argument. On many systems, all pointers
are interchangeable, so it doesn't matter, but the C++ standard doesn't
require this.

Note 2: 7.11p2 defines the conversion from a pointer to an object type
to a void*. There's no corresponding clause describing conversion from a
pointer to a function type to a void*, except 7.11p1, which only applies
to null pointers. The behavior of converting a non-null pointer to a
function to void* so it can be used by printf("%p") is therefore
undefined by omission of an explicit definition (3.27).
The best you can do with function pointer is to store in in an pointer
object, and then print a hex dump of the bytes that make up the pointer.

In practice, such code has good chance of working as you expect it to,
particularly if you're using a POSIX-compliant system: dlsym() can only
work as it's supposed to if the function pointers it works with can be
safely converted to and from void*. That doesn't guarantee that any
other function pointers can be safely converted, but it does make it
more likely.

> printf("%p\n", fid<decltype(f2), f2>());
> printf("%p\n", fid<decltype(f3), f3>());
> return 0;
> };
>
> CODE END------------------------------------------
>
>
> fid takes a function pointer as template parameter and returns the pointer to
> itself, making a kind of numeric identifer of a declaration.
> This is the entire source code, no additonal files are present.
> f1, f2, f3 are only delcared here, no definition is given.
>
> The question is:
> a) is this code valid (should this code compile)?
> b) if it is, what should it output: are three different pointers guaranteed?
> c) if it is not, why?
> Or, maybe, it is a some case of undefined behavior / no diagnostics required?
>
> My reasoning goes this way:
> Main question: Are f1, f2, f3 ODR used?

"A function whose name appears as a potentially-evaluated expression is
odr-used if it is the unique lookup result or the selected member of a
set of overloaded functions (6.4, 16.3, 16.4), unless it is a pure
virtual function and either its name is not explicitly qualified or the
expression forms a pointer to member (8.3.1)."

f1, f2, and f3 are the unique lookup results, so that clause should
apply - there are no overloaded or pure virtual functions involved, so
none of the exceptions apply.

> If yes, the code sould not compile.

I think it should compile - but what it should not do is link.

Bo Persson

unread,
Apr 23, 2018, 9:36:11 AM4/23/18
to
MSVC is known to merge functions with identical machine code. This helps
remove redundant template expansions, but also accidentally produces the
same address for different (but very similar) functions.



Bo Persson

Öö Tiib

unread,
Apr 23, 2018, 9:54:54 AM4/23/18
to
On Monday, 23 April 2018 15:11:37 UTC+3, kmo wrote:
> Please consider the following code:
>
> CODE START----------------------------------------
>
> #include<stdio.h>
>
> int f1(int, int);
> int f2(int, int);
> int f3(int);
>
> template<typename T, T *ptr>
> inline void *fid() {
> return (void *)&fid<T, ptr>;
> };
>
> int main() {
> printf("%p\n", fid<decltype(f1), f1>());
> printf("%p\n", fid<decltype(f2), f2>());
> printf("%p\n", fid<decltype(f3), f3>());
> return 0;
> };
>
> CODE END------------------------------------------
>
>
> fid takes a function pointer as template parameter and returns the pointer to
> itself, making a kind of numeric identifer of a declaration.
> This is the entire source code, no additonal files are present.
> f1, f2, f3 are only delcared here, no definition is given.
>
> The question is:
> a) is this code valid (should this code compile)?
> b) if it is, what should it output: are three different pointers guaranteed?
> c) if it is not, why?

Seems that it is not valid code and so may refuse to compile.

> Or, maybe, it is a some case of undefined behavior / no diagnostics required?
>
> My reasoning goes this way:
> Main question: Are f1, f2, f3 ODR used?

Yes, these have to be selected by overload resolution when addresses are
taken so must be ODR-used.

> If yes, the code sould not compile.
> If not, fid instances are different objects, so they should have different
> addresses.

Perhaps the reason why it compiles is that the usage of those function
pointers taken is optimized out so linker (unless it is whole program
optimizer) has no usages left to link to.

>
> Visual Studio 2017 (15.6.6) compiles and prints 3 equal pointers,
> unless you compile it with /GL (whole program optimization) in which case
> linker complains about unresolved f1, f2, f3.
> However, replacing every fid<decltype(f*), f*> with fid<decltype(f*), &f*>
> for *=1,2,3 silences the linker and the output is 3 equal pointers.
>
> Clang (bundled with the mentioned Visual Studio, LLVM-2014) also compiles, prints
> 3 different pointers. Compiling in Release produces 3 equal pointers.
>
> g++ 4.8.1 (MinGW) compiles and prints 3 different pointers.
> g++ 5.2.1 (Ubuntu 15.10) compiles, 3 different pointers.
> clang++ 3.6.2 (Ubuntu 15.10) compiles, 3 different pointers.
>
> Compilers on Compiler Explorer compile it just fine, but probably the code
> is only compiled, not linked (G++ 7.3, ICC 18.0.0, CLang 6.0.0, MSVC 19 2017 RTW).
>
> So the majority says a) yes, b) different, but is the majority correct?

I think the compilers are incorrect, but I can't imagine an actual problem,
some motivating example that their incorrectness would break for me.

Tim Rentsch

unread,
Apr 23, 2018, 4:12:17 PM4/23/18
to
kmo <th...@e.mail.invalid.is> writes:

> Please consider the following code:
>
> CODE START----------------------------------------
>
> #include<stdio.h>
>
> int f1(int, int);
> int f2(int, int);
> int f3(int);
>
> template<typename T, T *ptr>
> inline void *fid() {
> return (void *)&fid<T, ptr>;
> };
>
> int main() {
> printf("%p\n", fid<decltype(f1), f1>());
> printf("%p\n", fid<decltype(f2), f2>());
> printf("%p\n", fid<decltype(f3), f3>());
> return 0;
> };
>
> CODE END------------------------------------------
>
>
> fid takes a function pointer as template parameter and returns the
> pointer to itself, making a kind of numeric identifer of a
> declaration. This is the entire source code, no additonal files
> are present. f1, f2, f3 are only delcared here, no definition is
> given.
>
> The question is:
> a) is this code valid (should this code compile)?

Not counting the two extraneous semicolons and the question
regarding converting a pointer-to-function to a (void *), the
code is valid. That is, a C++ compiler must accept it as a
single translation unit.

Beyond that, I am not sure. Please read on.

> b) if it is, what should it output: are three different pointers
> guaranteed?
> c) if it is not, why?
> Or, maybe, it is a some case of undefined behavior / no
> diagnostics required?
> My reasoning goes this way:
> Main question: Are f1, f2, f3 ODR used?

That is indeed the key question. If they are ODR-used, and not
defined, then there is undefined behavior and all bets are off.

If they are not ODR-used, then it's a question about templates
that is beyond my current level of understanding.

I looked briefly (ie, at the C++ standard) to see if I could
figure out whether these functions are ODR-used. About the
only conclusion I reached is that finding out would take
longer than I wanted to spend. :(

> If yes, the code sould not compile.

As a single TU, it /should/ compile. The functions may be
defined in another translation unit.

As a whole program, compiling is allowed because of the undefined
behavior that results from not defining an identifier that is
ODR-used.

> If not, fid instances are different objects, so they should
> have different addresses.

I believe that's right. I'm not sure of the exact rules for
template instantiation but that's what I would expect in the
absence of information to the contrary. Also I'm not sure if
the presence of 'inline' in the definition of fid() might
influence that. As it compiles without 'inline', I might be
inclined to leave it off.

Tim Rentsch

unread,
Apr 23, 2018, 4:18:03 PM4/23/18
to
"James R. Kuyper" <james...@verizon.net> writes:

> On 04/23/2018 08:11 AM, kmo wrote:
>
>> Please consider the following code:
>>
>> CODE START----------------------------------------
>>
>> #include<stdio.h>
>>
>> int f1(int, int);
>> int f2(int, int);
>> int f3(int);
>>
>> template<typename T, T *ptr>
>> inline void *fid() {
>> return (void *)&fid<T, ptr>;
>> };
>>
>> int main() {
>> printf("%p\n", fid<decltype(f1), f1>());
>
> Note 1: "%p" requires a void* argument. On many systems, all pointers
> are interchangeable, so it doesn't matter, but the C++ standard
> doesn't require this.

Do you think the argument value is not of type (void*)? Did you
miss the () after the template invocation?

>> Main question: Are f1, f2, f3 ODR used?
>> If yes, the code sould not compile.
>
> I think it should compile - but what it should not do is link.

I believe it may link, because not defining the functions
results in undefined behavior.

james...@verizon.net

unread,
Apr 23, 2018, 4:43:08 PM4/23/18
to
On Monday, April 23, 2018 at 4:18:03 PM UTC-4, Tim Rentsch wrote:
> "James R. Kuyper" <james...@verizon.net> writes:
>
> > On 04/23/2018 08:11 AM, kmo wrote:
> >
> >> Please consider the following code:
> >>
> >> CODE START----------------------------------------
> >>
> >> #include<stdio.h>
> >>
> >> int f1(int, int);
> >> int f2(int, int);
> >> int f3(int);
> >>
> >> template<typename T, T *ptr>
> >> inline void *fid() {
> >> return (void *)&fid<T, ptr>;
> >> };
> >>
> >> int main() {
> >> printf("%p\n", fid<decltype(f1), f1>());
> >
> > Note 1: "%p" requires a void* argument. On many systems, all pointers
> > are interchangeable, so it doesn't matter, but the C++ standard
> > doesn't require this.
>
> Do you think the argument value is not of type (void*)? Did you
> miss the () after the template invocation?

I missed the explicit cast to void*, which is pretty dumb. My comments about that conversion having undefined behavior remain valid.

> >> Main question: Are f1, f2, f3 ODR used?
> >> If yes, the code sould not compile.
> >
> > I think it should compile - but what it should not do is link.
>
> I believe it may link, because not defining the functions
> results in undefined behavior.

True, undefined behavior allows for the possibility of it linking - but
as a matter of QoI, I think it shouldn't link. What I know about linking
doesn't suggest any reason why that should be difficult to achieve - but
I'm not by any means an expert on real world linkers.

Chris Vine

unread,
Apr 24, 2018, 5:55:02 AM4/24/18
to
On Mon, 23 Apr 2018 09:30:50 -0400
"James R. Kuyper" <james...@verizon.net> wrote:
[snip]
> Note 1: "%p" requires a void* argument. On many systems, all pointers
> are interchangeable, so it doesn't matter, but the C++ standard doesn't
> require this.
>
> Note 2: 7.11p2 defines the conversion from a pointer to an object type
> to a void*. There's no corresponding clause describing conversion from a
> pointer to a function type to a void*, except 7.11p1, which only applies
> to null pointers. The behavior of converting a non-null pointer to a
> function to void* so it can be used by printf("%p") is therefore
> undefined by omission of an explicit definition (3.27).
> The best you can do with function pointer is to store in in an pointer
> object, and then print a hex dump of the bytes that make up the pointer.
>
> In practice, such code has good chance of working as you expect it to,
> particularly if you're using a POSIX-compliant system: dlsym() can only
> work as it's supposed to if the function pointers it works with can be
> safely converted to and from void*. That doesn't guarantee that any
> other function pointers can be safely converted, but it does make it
> more likely.

There is provision in the C++ standard for conversions of function
pointer to object pointer and vice versa. They are conditionally
supported in C++, and the behaviour is implementation defined rather
than undefined. This is to support the POSIX requirement that you
mention:

"Converting a function pointer to an object pointer type or vice versa
is conditionally-supported. The meaning of such a conversion is
implementation-defined, except that if an implementation supports
conversions in both directions, converting a prvalue of one type to the
other type and back, possibly with different cv-qualification, shall
yield the original pointer value." (5.2.10/8 of C++14)

kmo

unread,
Apr 24, 2018, 6:55:07 AM4/24/18
to
james...@verizon.net wrote in
news:3bb55a23-5fdf-4798...@googlegroups.com:

> On Monday, April 23, 2018 at 4:18:03 PM UTC-4, Tim Rentsch wrote:
>> "James R. Kuyper" <james...@verizon.net> writes:
>>
>> > On 04/23/2018 08:11 AM, kmo wrote:
>> >
>> >> Please consider the following code:
>> >>
>> >> CODE START----------------------------------------
>> >>
>> >> #include<stdio.h>
>> >>
>> >> int f1(int, int);
>> >> int f2(int, int);
>> >> int f3(int);
>> >>
>> >> template<typename T, T *ptr>
>> >> inline void *fid() {
>> >> return (void *)&fid<T, ptr>;
>> >> };
>> >>
>> >> int main() {
>> >> printf("%p\n", fid<decltype(f1), f1>());
>> >
>> > Note 1: "%p" requires a void* argument. On many systems, all
>> > pointers are interchangeable, so it doesn't matter, but the C++
>> > standard doesn't require this.
>>
>> Do you think the argument value is not of type (void*)? Did you
>> miss the () after the template invocation?
>
> I missed the explicit cast to void*, which is pretty dumb. My comments
> about that conversion having undefined behavior remain valid.
>

Yes, that was my bad. This should fix the undefined behavior:
---------------------------------
template<typename T, T *ptr>
inline void *fid() {
static int t;
return &t;
};
---------------------------------
The results are almost the same. The only change is that now
Visual Studio produces different pointers in case where 3 equal
pointers were printed.

>> >> Main question: Are f1, f2, f3 ODR used?
>> >>
>> > "A function whose name appears as a potentially-evaluated expression is
>> > odr-used if it is the unique lookup result or the selected member of a
>> > set of overloaded functions (6.4, 16.3, 16.4), unless it is a pure
>> > virtual function and either its name is not explicitly qualified or the
>> > expression forms a pointer to member (8.3.1)."

Thank you for clarifying this fragment for me. I was staring at it
and could not decide if it applies to a template argument.

>> > I think it should compile - but what it should not do is link.
>>
>> I believe it may link, because not defining the functions
>> results in undefined behavior.
>
> True, undefined behavior allows for the possibility of it linking -
> but as a matter of QoI, I think it shouldn't link. What I know about
> linking doesn't suggest any reason why that should be difficult to
> achieve - but I'm not by any means an expert on real world linkers.

I see I have used "compile" in too broad sense. What I meant was
compile and link (since there is no other code).

Anyway, thank you for helping me understand this.
To sum up: f1, f2, f3 are ODR used but not defined
so it is an undefined behavior with no diagnostic required
(and indeed, there is no diagnostic in most cases).

kmo

unread,
Apr 24, 2018, 7:10:03 AM4/24/18
to
=?UTF-8?B?w5bDtiBUaWli?= <oot...@hot.ee> wrote in
news:54d40974-b67a-45a2...@googlegroups.com:
>
> I think the compilers are incorrect, but I can't imagine an actual
> problem, some motivating example that their incorrectness would break
> for me.
>

Actually, the problem came up when I was implementing a toy RPC.
I wanted the description of the RPC interface to be as concise as
possible -- ideally it would be a header file with declarations
of allowed calls. Therefore, I needed a method of creating some kind
of identifer based on a declaration to identify what needs to be called.

Anyway, the attempt failed. While it may work for most compilers now,
it can break at any time: it employs undefined behavior by not defining
an ODR used function, as explained by James R. Kuyper in other response.

James R. Kuyper

unread,
Apr 24, 2018, 8:48:44 AM4/24/18
to
Correction: 8.2.10p8, at least in my copy of C++17.
I'm still more familiar with C++98 than the newer versions of the
standard. I read p6, about function pointers, saw that p7 was about
object pointers, and apparently stopped reading because I failed to
notice that p8 was about both. As a conditionally-supported feature with
an implementation-defined meaning, it's still something to be avoided in
code that's intended to be portable.
0 new messages