C functions with "volatile" return types - what does it mean? - Page 1
Forum section: electronics / general

author: westfw
author_rank: Super Contributor
timestamp: October 07, 2024, 12:11:49 am
content: I was reading some freeRTOS documentation, and came across:Code:volatile TickType_t xTaskGetTickCount( void );I don't think I understand what "volatile" does here, in the function definition.A web search did not turn up any clear answers, except to clarify that this means that it is the return type (TickType_t) that is volatile, not "the function."But I don't understand what it means for a return value to be "volatile." Return values are almost exclusively put in some register, and they're effectively "frozen" from that point on.I suppose that this could prevent optimization from eliding multiple calls to the function:Code:x = xTaskGetTickCount();y = xTaskGetTickCount(); // yes, REALLY call the function again!Except that I thought that that was default behavior for function calls, perhaps overrideable with "pure" and "const" attributes (at least in gcc. Attributes are non-standard.)Is a volatile return type just the opposite of the const attribute, enshrined in standardization? Does it mean that at some point function return types WON'T be volatile by default? (that will break a lot of things, I think.) Does the optimization it's trying to prevent happen only when functions are in-lined? (and if so, isn't that redundant if the function body accesses volatile variables?)

author: SiliconWizard
author_rank: Super Contributor
timestamp: October 07, 2024, 12:52:52 am
content: Yes that would qualify the return type.I tried thinking of contexts in which it could make a difference, but couldn't find any.- If the function is defined in the same compilation unit, then whatever happens inside it will matter (as to whether it will be actually called or not) rather than its return type being qualified volatile or not.- If it's defined externally, then it will get called unconditionally anyway, as the compiler can't know what the side-effects inside of it could be.So, I don't think it has any practical purpose. Maybe it's in their "coding style" to match the return type exactly with the expression that is returned, although again here that shouldn't make any difference in terms of compiled code.What I mean is:xTaskGetTickCount() probably returns a global variable that is qualified volatile (a tick counter). So possibly their coding style requires the return type to match the returned expression, qualifiers included.Just a guess. That may not be the rationale, but I know they have strict coding guidelines.Now if anyone can find any real example of it making a difference, I'm all ears. I may have missed it.

author: radiolistener
author_rank: Super Contributor
timestamp: October 07, 2024, 01:06:22 am
content: I think it is likely marked as volatile to ensure that the compiler does not omit calls to xTaskGetTickCount() for optimization purposes, even if it assumes that the function xTaskGetTickCount() code will return the same value.

author: westfw
author_rank: Super Contributor
timestamp: October 07, 2024, 01:27:46 am
content: (Apparently this function is not really volatile; it seems to be a documentation error.)

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: October 07, 2024, 04:07:58 am
content: Quote from: westfw on October 07, 2024, 12:11:49 amWhat language are we talking here? C or C++?In C language it means absolutely nothing. In C the return value is an rvalue and C does not support cv-qualification of rvalues. Meaning that you can legally apply such qualification in the source code, but it will be ignored anyway.It is all natural aside from one corner case, which exists since C99: when the returned object is a struct with an array inside.Code:#include <stdio.h>volatile struct S { int a; } foo(void){return (struct S) { { 1, 2, 3, 4, 5 } };}int main(){int *p;p = foo().a + 2, printf("%d\n", *p);}Should the compiler complain about a pointer to non-volatile trying to point to a volatile object? The standard says that the answer is still the same: the cv-qualificatin of the returned value is ignored, so there's no violation in the above example.

author: Psi
author_rank: Super Contributor
timestamp: October 07, 2024, 04:28:10 am
content: I tried to think about this, but all it did was hurt my brain.Maybe it's one of those situations where you have to do something just to keep lint happy.

author: golden_labels
author_rank: Super Contributor
timestamp: October 07, 2024, 05:36:40 am
content: A qualifier placed before the type is actually an exception:(1)normally those go after the type. If it would be written in that pattern, you’d seeTickType_t volatile xTaskGetTickCount(void). The association withTickType_twould be clearer, with no doubts about whether it applies to the type returned, the function, or something else.It has been made clear in C99, and that interpretation continues with C23, that“(t)he properties associated with qualified types are meaningful only for expressions that are lvalues.”So for a return value it must be ignored and in fact gcc and clang will complain.(2)If that quirk in the documentation is a remnant of the actual use ofvolatileon a return type, there may be an explanation. In the past 15–20 years major C compiler vendors leaned towards adhering to the standard C. But before that, and it still lingers among niche and in-house toolsets, the advertised C support was in fact “we don’t support C, we support ‘C flavour’”. While “flavour” sounds like just a minor deviation, in reality some were going right against the very core logic of the language.(3)For example Microsoft was not permitting freeing memory from const-qualified pointers and that’s just a tip of the iceberg of code-breaking incompatibilities. It is possiblevolatilewas there at some point to circumvent or invoke some non-standard behavior of some old or niche compiler.(1)I don’t know the history of how it came into existence. The original K&R C had no type qualifiers (likeconst). However, it had type specifiers (likeshort) and storage class specifiers (likeregister), which were placed before the type. I reckon it’s possible C99 permitted that order, because early type qualifiers followed the pattern, until it was recognized it’s wrong and they should actually be placed after the type they qualify. But this is only my own guess.(2)Code:$ gcc -c -Wignored-qualifiers -o /dev/null wrong.cwrong.c:1:5: warning: type qualifiers ignored on function return type [-Wignored-qualifiers]1 | int const f(void);| ^~~~~$ clang -c -Wignored-qualifiers -o /dev/null wrong.cwrong.c:1:5: warning: 'const' type qualifier on return type has no effect [-Wignored-qualifiers]1 | int const f(void);|-Wignored-qualifiersis enabled by-Wextra.(3)And it shouldn’t be confused with what nowadays compilers may offer as an option, like clang and gcc with-std.

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: October 07, 2024, 05:48:56 am
content: Quote from: golden_labels on October 07, 2024, 05:36:40 amNo, there's nothing "normally" about it. Quite the opposite, the "normal" convention is to place it before the type. There are some individuals that seem to tout the "after the type" approach claiming there's some kind of logical rationale for it, but there really isn't.Quote from: golden_labels on October 07, 2024, 05:36:40 amThe authors of the declaration in question obviously did not adhere to the "after the type" convention.Quote from: golden_labels on October 07, 2024, 05:36:40 amNo, there's absolutely nothing clearer about it. In fact, one can argue that the "after the type" approach is misleading. That cv-qualifier belongs to the entire declaration, not to the first individual declarator. The "before the type" approach makes that perfectly clear, while the "after the type" approach tends to obfuscate that fact.

author: golden_labels
author_rank: Super Contributor
timestamp: October 07, 2024, 06:11:00 am
content: Quote from: TheCalligrapher on October 07, 2024, 05:48:56 amForgive me for being too dim, but in my limited mental capability this simple argument is very convincing: one can’t write it before the type in almost all cases. I think my mind is so feeble, that I may be persuaded to think otherwise by seeing one counter-example. Just one, tiny qualified type example,that is not covered by this specific exception.And, for others, a cabinet of curiosities:Code:int intCat(void);long longCat(void);long int stillLongCat(void);int long anotherLongCat(void);long int long veryLongCat(void);int short shortCat(void);int short signed oscillatingCat(void);long signed long longOscillatingCat(void);float floatingCat(void);double long ultraPreciseCat(void);// Strictly speaking valid, but avoid this one:long extern int const long signed mutantCat(void);

author: ejeffrey
author_rank: Super Contributor
timestamp: October 07, 2024, 03:30:06 pm
content: Quote from: Psi on October 07, 2024, 04:28:10 amI think it's just a mistake. Or an accidental side effect of poorly thought out code generation or preprocessor expansion. It might be something someone tries when they are attempting to make a lint or compiler warning they don't understand go away. No sensible tool should require it. A useful tool probably should complain about it on the grounds that if you think it does anything you need to fix your understanding.It could prevent optimizing out a function call, but that would be a compiler specific extension with no basis in the standard. And it's unclear how that would work in practice. The only way compilers optimize out function calls is if they first inline them. If you want to prevent inlining, compilers have much clearer extensions to specify this.

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: October 08, 2024, 03:57:04 am
content: Quote from: golden_labels on October 07, 2024, 06:11:00 amUm... What? I feel like there's some kind of gross misunderstanding involved here. Personally, I'm not aware of any cases where one "can’t write it before the type". Just consider thisCode:const int ANSWER = 42;Here you go. In the above example cv-qualifier `const` comes before the type. Nothing odd, unusual or surprising about it.

author: golden_labels
author_rank: Super Contributor
timestamp: October 08, 2024, 05:33:46 am
content: I asked for a case, that is not an exceptional one. This one is an exception. A simple (type, qualifier) pair, where qualifier may be placed before the type it applies to.Normally a type qualifier list only goes after the type it applies to, because it belongs to a declarator, which is a suffix to that type. Not only is this a result of C approximating context-free grammar, and permits very natural and intuitive “stacking”, but it’s even hard to imagine how it could be written otherwise without copious amount of parentheses.For reasons unknown to me, but for which a plausible explanation I suggested in a post above, there is an exception added. In declaration specifiers, which completely covers your (type, qualifier) pair without further parsing, a rule for qualifiers duplicates that of storage class specifiers (likeexternorauto).(1)An unintended consequence of this rule being added are “monstrosities” from the zoo above. Even something like this:Code:int typedef x;One note. I call that an exception, because I assume there is intentionality behind this. The intention would be allowing qualifiers to go before type in that special case. But this is nothing more than my assumption. Plain “laziness” without caring about allowing the prefix version would produce the same grammar, because it’s the easiest way to chain things. I prefer to believe that it was an exception added to allow people put things likeconstbefore types, but the alternative version is equally valid: unintended syntax, which happens to work in simple cases, becoming the convention.I hope that resolves the confusion. Because a single sentence, tangentially relevant and uttered only as an introduction to a paragraph discussing another thing, a paragraph itself short and followed by two others, is taking considerable part of this thread already.(1)C23 even dropped distinction between them in this context.

author: peter-h
author_rank: Super Contributor
timestamp: October 08, 2024, 06:32:42 am
content: QuoteIs that legal C compiler behaviour?Even if a function contained a constant expression like return(5) would the compiler be allowed to optimise away the call to it?

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: October 08, 2024, 06:55:58 am
content: Quote from: peter-h on October 08, 2024, 06:32:42 amC compilers follow "as if" principle when it comes to optimizations. The compiler is allowed to do absolutely anything, as long as theobservable behaviorof the program remains unchanged. But qualifying function return types as `volatile` does not make any difference in that regard. The compiler is allowed to omit absolutely any calls, as long as theobservable behavioris not affected.Since C compilers adhere to the principle ofindependent translation, the compiler is generally not allowed to assume anything about functions whose definitions are not present in the current translation unit. For this reason, compilers often provide extensions allowing the user to give it "hints" about the function's external behavior (e.g. `__attribute__((pure))`, `__attribute__((const))` and some such in GCC).But I doubt that `volatile` in question is intended to serve as such a hint.

author: peter-h
author_rank: Super Contributor
timestamp: October 08, 2024, 07:06:43 am
content: That suggests that a function call would not be optimised away if the function was not in the current .c file, yes?In the case of tick count, that should be ok because ultimately you are reading a hardware register or a memory address and that should be declared as volatile (usually in some .h file) so the call to tick count should not be optimised away no matter how much processing is done inside that function.

author: ejeffrey
author_rank: Super Contributor
timestamp: October 08, 2024, 03:21:03 pm
content: Quote from: peter-h on October 08, 2024, 07:06:43 amAgain, whether a function is "optimized away" is an implementation detail. In practice, traditionally calls across translation units wouldn't be optimized. But if you use LTO they can be. If you are programming in C++ you really want LTO, but for most C programmers it doesn't make a huge difference.QuoteCorrect. Properly declaring the behavior will result in the compiler doing the correct thing. The function may still be inlined, but the hardware memory accesses won't be eliminated.

author: ejeffrey
author_rank: Super Contributor
timestamp: October 08, 2024, 03:44:27 pm
content: Hmm...one possible edge case a volatile function might be intended for is to count as a step for the purposes of the forward progress guarantee. That is, it would prevent removing infinite loops with no side effects. I don't believe this is actually allowed by the standard, only volatile lvalue accesses count, not rvalues. And it would be a perverse way to accomplish that when having a volatile local variable would be sufficient. But I could imagine someone trying that, and I could imagine a compiler accidentally implementing that behavior.

author: peter-h
author_rank: Super Contributor
timestamp: October 08, 2024, 03:47:11 pm
content: I looked up LTO. Is this real in ARM32 GCC? I know the linker will recursively strip out code which is never called. But how can the linker implement non volatility?

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: October 08, 2024, 05:30:35 pm
content: Quote from: peter-h on October 08, 2024, 07:06:43 amYes, precisely.Modern implementations always try to expand the optimization capabilities, which includesglobaloptimizations (aka "link-time" optimizations, LTOs). But I would guess that eliminating "unnecessary" function calls does not rank too high on the list of their development priorities. Users don't normally make unnecessary function calls.Quote from: ejeffrey on October 08, 2024, 03:21:03 pmI don't see how and why it would be different for C++ users vs. C users. Why really?

author: golden_labels
author_rank: Super Contributor
timestamp: October 08, 2024, 05:47:56 pm
content: In C this kind ofvolatileusage is meaningless, so a note should be taken: we’re now discussing a hypothetical behavior of what would be compiler-specific extension. Almost unconstrained by how C works.While most often mentioned use ofvolatileis breaking continuity, there is another one: volatile operations form a totally ordered set. So a hypothetical use ofvolatileon a function could be interpreted as making a function a part of that set.

author: peter-h
author_rank: Super Contributor
timestamp: October 08, 2024, 06:12:22 pm
content: QuoteNot intentionally, but a while ago my 450k of code suddenly became 12k - because my boot loader was entering main() via a long jump at base+32k (I will spare you the details; already comprehensively posted elsewhere) and the compiler was not clever enough to spot the jump, so it optimised away main() and everything inside itUltimately this debate is:We, the GCC devs, produced a really good compiler around 2010, but we can't make ourselves unemployed, so what do we do? We go sideways... we go and look for weird ways to optimise away bits of code which the writer never wanted to get rid of.I mean, somebody tell me, how much code is needed to recognise some loop structure, determine its limits, and replace it with memcpy or memset? They probably spent many man-months just doing that.

author: Siwastaja
author_rank: Super Contributor
timestamp: October 08, 2024, 06:59:47 pm
content: Quote from: peter-h on October 08, 2024, 06:32:42 amOf course. Optimization is your friend, not your enemy.

author: xvr
author_rank: Frequent Contributor
timestamp: October 08, 2024, 07:03:42 pm
content: Quote from: peter-h on October 08, 2024, 03:47:11 pmIt could, because it not a linker in LTOIn LTO mode compiler do not generate code when compile source files, instead it produced IR (Intermediate Representation) of it (just parsed and machine readable form of source code).On LTO link time compiler called again with all IR from all compilation units. This is almost the same as compiling whole program as one big *.c file. So compiler has all possibility to inline and remove whatever it want.

author: westfw
author_rank: Super Contributor
timestamp: October 08, 2024, 09:41:17 pm
content: QuoteQuoteThis is what the (not standardized, yet) "pure" and "const" __attributes__ are for, at least theoretically. If the .h file specifies that sin() has the const attribute, then the compiler can decide that multiple uses of sin(5) could be optimized away. (I checked a couple math.h files, and they DON'T do this.)

author: SiliconWizard
author_rank: Super Contributor
timestamp: October 08, 2024, 10:36:42 pm
content: Quote from: golden_labels on October 08, 2024, 05:47:56 pmThat's right. Yes, the OP has said long ago now that it was actually not in the code itself but a documentation error (maybe coming from older, actual code with some dev who had put this volatile qualifier thinking it would be needed, while it was definitely not).So, yes, from that point on, it's all purely hypothetical, but the bottom line is that volatile for qualifying a function's return value in C is meaningless, and qualifying the function itself is not a thing, apart from "static".I haven't yet looked into the new attributes westfw talks about in his last post above. But from what I get, those would be attributes, and not qualifiers. And nothing about being "volatile".

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 08, 2024, 11:07:48 pm
content: This is actually a pretty good illustration whytypedef'ing pointer types in C can lead to unexpected behaviour.Let's say you havevolatile char *example(void);Because thevolatileis on the left side of the asterisk, it applies to the pointed to value. Writingchar *volatile example(void);means the pointer itself is volatile, not the pointed to value.Now, let's say you havetypedef char * mytype;volatile mytype example(void);Do you know what thevolatilerefers to this time?Yes, to the pointer, and not to the pointed-to value. That is, it is equivalent tochar *volatile example(void);and not tovolatile char *example(void);If you accept thattypedefis not just shorthand, that it isn't equivalent to a preprocessor macro definition (#define mytype char *would behave the exact opposite way), and that it actually, really defines a new type, it makes perfect sense.It also means that defining anopaque cookietype, saytypedef void * handle;makes perfect sense: it is an unknown thing you obtain from somewhere and pass along as needed, but can never examine in detail, only as far as its address (and even that in limited ways, because of C's pointer rules). Even though it is a pointer, it points to void, so you cannot dereference it as-is, either. Doing stuff likeconst handle h = ...;in a function makes perfect sense too. It means the cookie, the variablehis const, that us programmers promise to not try and change its value in the current scope. It says nothing about what it might point to.

author: SiliconWizard
author_rank: Super Contributor
timestamp: October 08, 2024, 11:32:53 pm
content: Good points. I personally don't think that the typedef'ing here is confusing - actually it's less confusing than the direct declaration. And yes, typedef defines a new type. I understand why you'd point it out though, as indeed, it looks like many C devs (who unfortunately have very varying levels of skills, as we regularly talk about) do not really understand types, typedef and even the point of defining types (which is even more concerning, but hey.)C declarations are sure a very fun thing to toy with, if you're up for some fun.Code:volatile char * volatile ptr;const volatile char * volatile ptr;We can spice things up.Code:const int * (* const foo)(volatile char * const);It's fun to see that this last declaration makes cdecl.org choke:https://cdecl.org/But it's valid C.

author: ejeffrey
author_rank: Super Contributor
timestamp: October 09, 2024, 04:55:10 am
content: Quote from: TheCalligrapher on October 08, 2024, 05:30:35 pmMostly that typical C++ has a lot more thin wrapper functions that are extremely amenable to inlining and not all of them are defined in headers. Also, it gives more chances for devirtulaization, which is rarely possible or necessary in C.

author: DiTBho
author_rank: Super Contributor
timestamp: October 10, 2024, 02:44:58 am
content: Quote from: Siwastaja on October 08, 2024, 06:59:47 pmIn 2005 Shirley Manson sang "Sex Is Not the Enemy" (Bleed Like Me)dressing a lovely "I (L) Nerds" t-shirt2024, that's a new hype for a t-shirt pretty nerdy t-shirt (Youtube Channel)

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: October 10, 2024, 04:57:16 am
content: Quote from: SiliconWizard on October 08, 2024, 11:32:53 pmA minimized example would beCode:void foo(char *const);cdecl.org apparently rejects top-level `const` on function arguments for some reason.

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 10, 2024, 05:25:08 am
content: Quote from: TheCalligrapher on October 10, 2024, 04:57:16 amAs far as I understand, in function declarations parameterconstness is irrelevant. That is, declarationint sum(int, int);for implementationint sum(int const x, int const y) { return x+y; }is perfectly valid, and in both cases the variables are of typeint; theconstness is only relevant to the function implementation (body).

author: SiliconWizard
author_rank: Super Contributor
timestamp: October 10, 2024, 05:36:45 am
content: I didn't go as far as trying to debug cdecl, but now that you pinpointed that, a few tests show that it actually rejects only const pointers as function arguments, but not const arguments other than const pointers, probably due to the odd-ish grammar that it doesn't handle correctly. So:void foo(const int *)is ok with cdecl, but not:void foo(int * const)No need to try and find a justification IMHO, it's clearly just a bug, because it will otherwise happily analyze const arguments in all other cases, like this:void foo(const int)and otherwise it's also happy with this:int * const fooSo, it's just const pointers in the context of function arguments that it rejects.Regarding relevance, sure it doesn't bring anything for function prototypes only, except of course pointers to const (const xxx *), and is meaningful only inside a function's body as NA mentioned, but that's obviously not a rationale for rejecting that. In terms of code style, writing a prototype that is different from the declaration itself would be quite odd.For people who would happen to have some time to kill, they provide the source code:https://github.com/ridiculousfish/cdecl-blocksinspecting the Yacc grammar file may be enough to figure it out. Or not.

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: October 10, 2024, 05:41:52 am
content: Quote from: Nominal Animal on October 10, 2024, 05:25:08 amFirstly, it is "irrelevant" in a sense that it does not affect function type, i.e. it does not affect external behavior of the function. The top-level cv-qualifiers applied to the parameters are ignored when determining function type. For example, a pointer of `void (*)(int)` type can be initialized with address of `void (const int)` function without an explicit cast. The types are the same. At the same time, that `const` is perfectly relevant to the internal implementation of the function: a parameter declared `const` is indeed seen as `const` inside the function.Secondly, relevant or irrelevant, it is still a perfectly valid C declaration. It is weird that cdecl reports it as a "syntax error". It isn't.P.S. BTW, I remember that a while ago I tried to find the legal wording in the standard that would state that `int sum(int, int);` prototype is a proper match to `int sum(const int, const int) { ... }` definition. Buit I couldn't. I eventually abandoned the question without finding any answer. Although, that was probably about C++, not C...

author: DiTBho
author_rank: Super Contributor
timestamp: October 10, 2024, 05:46:49 am
content: Quote from: TheCalligrapher on October 10, 2024, 05:41:52 amSierraC: syntax errorAvogetC: syntax errorDiabC: okGcc-v12: okfour C compilers tried, 50% OKSo, they probably used some compiler that was inclined not to accept that syntax

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: October 10, 2024, 06:06:06 am
content: Quote from: DiTBho on October 10, 2024, 05:46:49 amWhat exactly did you compile? Did you try to include a parameter name? E.g. `void foo(int const a)`?For example, thisCode:void foo(int const) {}is indeed a syntax error in all versions of C before C23. "Classic" C does not permit omitting parameter names in functiondefinitions.

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 10, 2024, 06:36:46 am
content: Quote from: TheCalligrapher on October 10, 2024, 05:41:52 amYep, that was what I tried to say.Quote from: TheCalligrapher on October 10, 2024, 05:41:52 amExactly. We are in perfect agreement.Quote from: TheCalligrapher on October 10, 2024, 05:41:52 amFully agreed.Apologies if I sounded otherwise; I do thinkcdeclbehaviour here is definitely in error.Quote from: TheCalligrapher on October 10, 2024, 05:41:52 amI also tried but couldn't, not for nor against. On my part, it is more an observation that a specific interpretation of the standard (includingconstbeing only meaningful in lvalue-type expressions) is in perfect alignment with all the C compilers I've used.Quote from: SiliconWizard on October 10, 2024, 05:36:45 amYep, this is exactly why I tend to include the parameterconstness even in the declaration in a header file, regardless of what I said above.Quote from: SiliconWizard on October 10, 2024, 05:36:45 amWhen reading.yfiles I first ask why I need to do this, then I lalr, and my brain turns to bison dung.

author: DiTBho
author_rank: Super Contributor
timestamp: October 10, 2024, 04:29:21 pm
content: Quote from: TheCalligrapher on October 10, 2024, 06:06:06 amyupCode:void foo(int* const a){...}